Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit a3b6b7242a22a8a97738eb4197bf6f63833a2f00


Parents : 36b6fd7
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-14T16:26:41-05:00

feat(tests): various cleanups, add JSON schema definitions and contract tests for API responses.

Changes

69 files changed, 2964 insertions(+), 2035 deletions(-)


Diff

diff --git a/tests/backend/api_json_contract_schemas.py b/tests/backend/api_json_contract_schemas.py
new file mode 100644
index 00000000..97ed8c90
--- /dev/null
+++ b/tests/backend/api_json_contract_schemas.py
@@ -0,0 +1,285 @@
+"""JSON Schema definitions for stable /api/v1 JSON bodies (contract tests)."""
+
+from __future__ import annotations
+
+from jsonschema import Draft202012Validator
+
+_USER_GUIDANCE_ITEM = {
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "description",
+ "action_route",
+ "action_label",
+ "severity",
+ ],
+ "properties": {
+ "id": {"type": "string"},
+ "title": {"type": "string"},
+ "description": {"type": "string"},
+ "action_route": {"type": "string"},
+ "action_label": {"type": "string"},
+ "severity": {"type": "string"},
+ },
+ "additionalProperties": True,
+}
+
+APP_INFO_BODY_SCHEMA: dict = {
+ "type": "object",
+ "required": [
+ "version",
+ "lxmf_version",
+ "rns_version",
+ "lxst_version",
+ "python_version",
+ "dependencies",
+ "storage_path",
+ "database_path",
+ "database_file_size",
+ "database_files",
+ "sqlite",
+ "reticulum_config_path",
+ "is_connected_to_shared_instance",
+ "shared_instance_address",
+ "is_transport_enabled",
+ "memory_usage",
+ "network_stats",
+ "reticulum_stats",
+ "is_reticulum_running",
+ "download_stats",
+ "emergency",
+ "integrity_issues",
+ "database_health_issues",
+ "user_guidance",
+ "tutorial_seen",
+ "changelog_seen_version",
+ ],
+ "properties": {
+ "version": {"type": "string"},
+ "lxmf_version": {"type": "string"},
+ "rns_version": {"type": "string"},
+ "lxst_version": {"type": "string"},
+ "python_version": {"type": "string"},
+ "dependencies": {
+ "type": "object",
+ "minProperties": 1,
+ "additionalProperties": {"type": "string"},
+ },
+ "storage_path": {"type": "string"},
+ "database_path": {"type": "string"},
+ "database_file_size": {"type": "integer"},
+ "database_files": {
+ "type": "object",
+ "required": ["main_bytes", "wal_bytes", "shm_bytes", "total_bytes"],
+ "properties": {
+ "main_bytes": {"type": "integer"},
+ "wal_bytes": {"type": "integer"},
+ "shm_bytes": {"type": "integer"},
+ "total_bytes": {"type": "integer"},
+ },
+ "additionalProperties": True,
+ },
+ "sqlite": {
+ "type": "object",
+ "required": [
+ "journal_mode",
+ "synchronous",
+ "wal_autocheckpoint",
+ "busy_timeout",
+ ],
+ "additionalProperties": True,
+ },
+ "reticulum_config_path": {"type": ["string", "null"]},
+ "is_connected_to_shared_instance": {"type": "boolean"},
+ "shared_instance_address": {"type": ["string", "null"]},
+ "is_transport_enabled": {"type": "boolean"},
+ "memory_usage": {
+ "type": "object",
+ "required": ["rss", "vms"],
+ "properties": {
+ "rss": {"type": "integer"},
+ "vms": {"type": "integer"},
+ },
+ "additionalProperties": True,
+ },
+ "network_stats": {
+ "type": "object",
+ "required": [
+ "bytes_sent",
+ "bytes_recv",
+ "packets_sent",
+ "packets_recv",
+ ],
+ "properties": {
+ "bytes_sent": {"type": "integer"},
+ "bytes_recv": {"type": "integer"},
+ "packets_sent": {"type": "integer"},
+ "packets_recv": {"type": "integer"},
+ },
+ "additionalProperties": True,
+ },
+ "reticulum_stats": {
+ "type": "object",
+ "required": [
+ "total_paths",
+ "announces_per_second",
+ "announces_per_minute",
+ "announces_per_hour",
+ ],
+ "properties": {
+ "total_paths": {"type": "integer"},
+ "announces_per_second": {"type": "integer"},
+ "announces_per_minute": {"type": "integer"},
+ "announces_per_hour": {"type": "integer"},
+ },
+ "additionalProperties": True,
+ },
+ "is_reticulum_running": {"type": "boolean"},
+ "download_stats": {
+ "type": "object",
+ "required": ["avg_download_speed_bps"],
+ "properties": {
+ "avg_download_speed_bps": {"type": ["number", "null"]},
+ },
+ "additionalProperties": True,
+ },
+ "emergency": {"type": "boolean"},
+ "integrity_issues": {"type": "array"},
+ "database_health_issues": {"type": "array"},
+ "user_guidance": {
+ "type": "array",
+ "items": _USER_GUIDANCE_ITEM,
+ },
+ "tutorial_seen": {"type": "boolean"},
+ "changelog_seen_version": {"type": "string"},
+ },
+ "additionalProperties": True,
+}
+
+API_V1_STATUS_SCHEMA: dict = {
+ "type": "object",
+ "required": ["status"],
+ "properties": {"status": {"type": "string", "const": "ok"}},
+ "additionalProperties": False,
+}
+
+API_V1_APP_INFO_ENVELOPE_SCHEMA: dict = {
+ "type": "object",
+ "required": ["app_info"],
+ "properties": {"app_info": APP_INFO_BODY_SCHEMA},
+ "additionalProperties": False,
+}
+
+AUTH_STATUS_SCHEMA: dict = {
+ "type": "object",
+ "required": ["auth_enabled", "password_set", "authenticated"],
+ "properties": {
+ "auth_enabled": {"type": "boolean"},
+ "password_set": {"type": "boolean"},
+ "authenticated": {"type": "boolean"},
+ "error": {"type": "string"},
+ },
+ "additionalProperties": False,
+}
+
+TELEPHONE_VOICEMAIL_STATUS_SCHEMA: dict = {
+ "type": "object",
+ "required": [
+ "has_espeak",
+ "has_ffmpeg",
+ "is_recording",
+ "is_greeting_recording",
+ "has_greeting",
+ ],
+ "properties": {
+ "has_espeak": {"type": "boolean"},
+ "has_ffmpeg": {"type": "boolean"},
+ "is_recording": {"type": "boolean"},
+ "is_greeting_recording": {"type": "boolean"},
+ "has_greeting": {"type": "boolean"},
+ },
+ "additionalProperties": False,
+}
+
+TELEPHONE_VOICEMAILS_ENVELOPE_SCHEMA: dict = {
+ "type": "object",
+ "required": ["voicemails", "unread_count"],
+ "properties": {
+ "voicemails": {
+ "type": "array",
+ "items": {"type": "object", "additionalProperties": True},
+ },
+ "unread_count": {"type": "integer"},
+ },
+ "additionalProperties": False,
+}
+
+_RINGTONE_ROW_SCHEMA: dict = {
+ "type": "object",
+ "required": ["id", "filename", "display_name", "is_primary", "created_at"],
+ "properties": {
+ "id": {"type": "integer"},
+ "filename": {"type": "string"},
+ "display_name": {"type": "string"},
+ "is_primary": {"type": "boolean"},
+ "created_at": {"type": ["string", "null"]},
+ },
+ "additionalProperties": True,
+}
+
+TELEPHONE_RINGTONES_LIST_SCHEMA: dict = {
+ "type": "array",
+ "items": _RINGTONE_ROW_SCHEMA,
+}
+
+TELEPHONE_RINGTONE_STATUS_SCHEMA: dict = {
+ "type": "object",
+ "required": [
+ "has_custom_ringtone",
+ "enabled",
+ "filename",
+ "id",
+ "volume",
+ ],
+ "properties": {
+ "has_custom_ringtone": {"type": "boolean"},
+ "enabled": {"type": "boolean"},
+ "filename": {"type": ["string", "null"]},
+ "id": {"type": ["integer", "null"]},
+ "volume": {"type": "number"},
+ },
+ "additionalProperties": False,
+}
+
+TELEPHONE_CONTACTS_LIST_SCHEMA: dict = {
+ "type": "object",
+ "required": ["contacts", "total_count"],
+ "properties": {
+ "contacts": {
+ "type": "array",
+ "items": {"type": "object", "additionalProperties": True},
+ },
+ "total_count": {"type": "integer"},
+ },
+ "additionalProperties": False,
+}
+
+TELEPHONE_CONTACT_CHECK_SCHEMA: dict = {
+ "type": "object",
+ "required": ["is_contact", "contact"],
+ "properties": {
+ "is_contact": {"type": "boolean"},
+ "contact": {
+ "oneOf": [
+ {"type": "null"},
+ {"type": "object", "additionalProperties": True},
+ ],
+ },
+ },
+ "additionalProperties": False,
+}
+
+
+def assert_matches_schema(instance: object, schema: dict) -> None:
+ Draft202012Validator(schema).validate(instance)

diff --git a/tests/backend/fixtures/http_api_routes.json b/tests/backend/fixtures/http_api_routes.json
index 688319ae..7e3db99c 100644
--- a/tests/backend/fixtures/http_api_routes.json
+++ b/tests/backend/fixtures/http_api_routes.json
@@ -1,960 +1,964 @@
{
- "routes": [
- {
- "method": "GET",
- "path": "/"
- },
- {
- "method": "GET",
- "path": "/api/v1/announce"
- },
- {
- "method": "GET",
- "path": "/api/v1/announces"
- },
- {
- "method": "GET",
- "path": "/api/v1/app/changelog"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/changelog/seen"
- },
- {
- "method": "GET",
- "path": "/api/v1/app/info"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/integrity/acknowledge"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/shutdown"
- },
- {
- "method": "POST",
- "path": "/api/v1/app/tutorial/seen"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/login"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/logout"
- },
- {
- "method": "POST",
- "path": "/api/v1/auth/setup"
- },
- {
- "method": "GET",
- "path": "/api/v1/auth/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/blocked-destinations"
- },
- {
- "method": "POST",
- "path": "/api/v1/blocked-destinations"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/blocked-destinations/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/delete"
- },
- {
- "method": "GET",
- "path": "/api/v1/bots/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/restart"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/start"
- },
- {
- "method": "GET",
- "path": "/api/v1/bots/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/bots/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/community-interfaces"
- },
- {
- "method": "GET",
- "path": "/api/v1/comports"
- },
- {
- "method": "GET",
- "path": "/api/v1/config"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/config"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/backup"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backup/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backups"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/database/backups/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/backups/{filename}/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/health"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/recover"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/restore"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/restore"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/snapshot"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/snapshots"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/database/snapshots/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/database/snapshots/{filename}/download"
- },
- {
- "method": "POST",
- "path": "/api/v1/database/vacuum"
- },
- {
- "method": "GET",
- "path": "/api/v1/debug/access-attempts"
- },
- {
- "method": "GET",
- "path": "/api/v1/debug/logs"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/custom-display-name"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/custom-display-name/update"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/drop-path"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/lxmf-stamp-info"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/path"
- },
- {
- "method": "POST",
- "path": "/api/v1/destination/{destination_hash}/request-path"
- },
- {
- "method": "GET",
- "path": "/api/v1/destination/{destination_hash}/signal-metrics"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/export"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/search"
- },
- {
- "method": "GET",
- "path": "/api/v1/docs/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/docs/switch"
- },
- {
- "method": "POST",
- "path": "/api/v1/docs/update"
- },
- {
- "method": "POST",
- "path": "/api/v1/docs/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/docs/version/{version}"
- },
- {
- "method": "GET",
- "path": "/api/v1/favourites"
- },
- {
- "method": "POST",
- "path": "/api/v1/favourites/add"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/favourites/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/favourites/{destination_hash}/rename"
- },
- {
- "method": "GET",
- "path": "/api/v1/identities"
- },
- {
- "method": "POST",
- "path": "/api/v1/identities/create"
- },
- {
- "method": "GET",
- "path": "/api/v1/identities/export-all"
- },
- {
- "method": "POST",
- "path": "/api/v1/identities/switch"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/identities/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/identity/backup/base32"
- },
- {
- "method": "GET",
- "path": "/api/v1/identity/backup/download"
- },
- {
- "method": "POST",
- "path": "/api/v1/identity/restore"
- },
- {
- "method": "GET",
- "path": "/api/v1/interface-stats"
- },
- {
- "method": "GET",
- "path": "/api/v1/licenses"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/send"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf-messages/{hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/{hash}/cancel"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf-messages/{hash}/spam"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf-messages/{message_hash}/uri"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/conversation-pins"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversation-pins/toggle"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/conversations"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/bulk-delete"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/bulk-mark-as-read"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/move-to-folder"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/conversations/{destination_hash}/mark-as-read"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/folders"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/folders"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/folders/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/lxmf/folders/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/lxmf/folders/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/lxmf/folders/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/stop-sync"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-node/sync"
- },
- {
- "method": "GET",
- "path": "/api/v1/lxmf/propagation-nodes"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/announces"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/archives"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/docs/reticulum"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/favourites"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/lxmf-icons"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/messages"
- },
- {
- "method": "GET",
- "path": "/api/v1/maintenance/messages/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/maintenance/messages/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/maintenance/stickers"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/drawings"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/drawings"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/drawings/{drawing_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/map/drawings/{drawing_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/export"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/export/{export_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/export/{export_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/export/{export_id}/download"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/mbtiles"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/mbtiles/active"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/map/mbtiles/{filename}"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/offline"
- },
- {
- "method": "POST",
- "path": "/api/v1/map/offline"
- },
- {
- "method": "GET",
- "path": "/api/v1/map/tiles/{z}/{x}/{y}"
- },
- {
- "method": "GET",
- "path": "/api/v1/meshchatx-docs/content"
- },
- {
- "method": "GET",
- "path": "/api/v1/meshchatx-docs/list"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/nomadnet/archives"
- },
- {
- "method": "GET",
- "path": "/api/v1/nomadnet/archives"
- },
- {
- "method": "POST",
- "path": "/api/v1/nomadnetwork/{destination_hash}/identify"
- },
- {
- "method": "GET",
- "path": "/api/v1/notifications"
- },
- {
- "method": "POST",
- "path": "/api/v1/notifications/mark-as-viewed"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/announce"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/files"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/files"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}/files/{file_name}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/pages"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/pages"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
- },
- {
- "method": "GET",
- "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
- },
- {
- "method": "PUT",
- "path": "/api/v1/page-nodes/{node_id}/rename"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/page-nodes/{node_id}/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/path-table"
- },
- {
- "method": "POST",
- "path": "/api/v1/path-table"
- },
- {
- "method": "GET",
- "path": "/api/v1/ping/{destination_hash}/lxmf.delivery"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/blackhole"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/disable-transport"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/discovered-interfaces"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/discovery"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/reticulum/discovery"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/enable-transport"
- },
- {
- "method": "GET",
- "path": "/api/v1/reticulum/interfaces"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/add"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/delete"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/disable"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/enable"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/import"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/interfaces/import-preview"
- },
- {
- "method": "POST",
- "path": "/api/v1/reticulum/reload"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/fetch"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/listen"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/send"
- },
- {
- "method": "GET",
- "path": "/api/v1/rncp/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/rncp/stop"
- },
- {
- "method": "GET",
- "path": "/api/v1/rncp/transfer/{transfer_id}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop-queues"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/drop-via"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/rates"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnpath/request"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/table"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnpath/trace/{destination_hash}"
- },
- {
- "method": "POST",
- "path": "/api/v1/rnprobe"
- },
- {
- "method": "GET",
- "path": "/api/v1/rnstatus"
- },
- {
- "method": "GET",
- "path": "/api/v1/spam-keywords"
- },
- {
- "method": "POST",
- "path": "/api/v1/spam-keywords"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/spam-keywords/{keyword_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers"
- },
- {
- "method": "POST",
- "path": "/api/v1/stickers"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/stickers/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/stickers/{sticker_id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/stickers/{sticker_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/stickers/{sticker_id}/image"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/history/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/latest/{destination_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/peers"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/tracking"
- },
- {
- "method": "POST",
- "path": "/api/v1/telemetry/tracking/{destination_hash}/toggle"
- },
- {
- "method": "GET",
- "path": "/api/v1/telemetry/trusted-peers"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/answer"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/audio-profiles"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/call/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/contacts"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts/check/{identity_hash}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/contacts/export"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/contacts/import"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/contacts/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/telephone/contacts/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/hangup"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/history"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/history"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/mute-receive"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/mute-transmit"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/recordings"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/recordings/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/recordings/{id}/audio/{side}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones/status"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/ringtones/upload"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/ringtones/{id}"
- },
- {
- "method": "PATCH",
- "path": "/api/v1/telephone/ringtones/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/ringtones/{id}/audio"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/send-to-voicemail"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/switch-audio-profile/{profile_id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/unmute-receive"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/unmute-transmit"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/generate-greeting"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/voicemail/greeting"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemail/greeting/audio"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/record/start"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/record/stop"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemail/greeting/upload"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemail/status"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemails"
- },
- {
- "method": "DELETE",
- "path": "/api/v1/telephone/voicemails/{id}"
- },
- {
- "method": "GET",
- "path": "/api/v1/telephone/voicemails/{id}/audio"
- },
- {
- "method": "POST",
- "path": "/api/v1/telephone/voicemails/{id}/read"
- },
- {
- "method": "GET",
- "path": "/api/v1/tools/rnode/download_firmware"
- },
- {
- "method": "POST",
- "path": "/api/v1/translator/install-languages"
- },
- {
- "method": "GET",
- "path": "/api/v1/translator/languages"
- },
- {
- "method": "POST",
- "path": "/api/v1/translator/translate"
- },
- {
- "method": "GET",
- "path": "/call.html"
- },
- {
- "method": "GET",
- "path": "/manifest.json"
- },
- {
- "method": "GET",
- "path": "/service-worker.js"
- },
- {
- "method": "GET",
- "path": "/ws"
- },
- {
- "method": "GET",
- "path": "/ws/telephone/audio"
- }
- ]
+ "routes": [
+ {
+ "method": "GET",
+ "path": "/"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/announce"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/announces"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/app/changelog"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/changelog/seen"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/app/info"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/integrity/acknowledge"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/shutdown"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/app/tutorial/seen"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/login"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/logout"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/auth/setup"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/auth/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/blocked-destinations"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/blocked-destinations"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/blocked-destinations/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/delete"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/bots/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/restart"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/start"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/bots/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/bots/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/community-interfaces"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/comports"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/config"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/config"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/backup"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backup/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backups"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/database/backups/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/backups/{filename}/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/health"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/recover"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/restore"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/restore"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/snapshot"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/snapshots"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/database/snapshots/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/database/snapshots/{filename}/download"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/database/vacuum"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/debug/access-attempts"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/debug/logs"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/custom-display-name"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/custom-display-name/update"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/drop-path"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/lxmf-stamp-info"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/path"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/destination/{destination_hash}/request-path"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/destination/{destination_hash}/signal-metrics"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/export"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/search"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/docs/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/docs/switch"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/docs/update"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/docs/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/docs/version/{version}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/favourites"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/favourites/add"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/favourites/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/favourites/{destination_hash}/rename"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identities"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identities/create"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identities/export-all"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identities/switch"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/identities/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identity/backup/base32"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/identity/backup/download"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/identity/restore"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/interface-stats"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/licenses"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/attachment/{message_hash}/{attachment_type}"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/conversation/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/reactions"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/send"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf-messages/{hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/{hash}/cancel"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf-messages/{hash}/spam"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf-messages/{message_hash}/uri"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/conversation-pins"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversation-pins/toggle"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/conversations"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/bulk-delete"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/bulk-mark-as-read"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/move-to-folder"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/conversations/{destination_hash}/mark-as-read"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/folders"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/folders"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/folders/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/lxmf/folders/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/lxmf/folders/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/lxmf/folders/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/stop-sync"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-node/sync"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/lxmf/propagation-nodes"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/announces"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/archives"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/docs/reticulum"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/favourites"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/lxmf-icons"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/messages"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/maintenance/messages/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/maintenance/messages/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/maintenance/stickers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/drawings"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/drawings"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/drawings/{drawing_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/map/drawings/{drawing_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/export"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/export/{export_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/export/{export_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/export/{export_id}/download"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/mbtiles"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/mbtiles/active"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/map/mbtiles/{filename}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/offline"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/map/offline"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/map/tiles/{z}/{x}/{y}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/meshchatx-docs/content"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/meshchatx-docs/list"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/nomadnet/archives"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/nomadnet/archives"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/nomadnetwork/{destination_hash}/identify"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/notifications"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/notifications/mark-as-viewed"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/announce"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/files"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/files"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}/files/{file_name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/pages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/pages"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/page-nodes/{node_id}/pages/{page_name}"
+ },
+ {
+ "method": "PUT",
+ "path": "/api/v1/page-nodes/{node_id}/rename"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/page-nodes/{node_id}/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/path-table"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/path-table"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/ping/{destination_hash}/lxmf.delivery"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/blackhole"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/disable-transport"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/discovered-interfaces"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/discovery"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/reticulum/discovery"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/enable-transport"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/reticulum/interfaces"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/add"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/delete"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/disable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/enable"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/import"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/interfaces/import-preview"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/reticulum/reload"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/fetch"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/listen"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/send"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rncp/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rncp/stop"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rncp/transfer/{transfer_id}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop-queues"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/drop-via"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/rates"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnpath/request"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/table"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnpath/trace/{destination_hash}"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/rnprobe"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/rnstatus"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/spam-keywords"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/spam-keywords"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/spam-keywords/{keyword_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/stickers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/stickers/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/stickers/{sticker_id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/stickers/{sticker_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/stickers/{sticker_id}/image"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/history/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/latest/{destination_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/peers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/tracking"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telemetry/tracking/{destination_hash}/toggle"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telemetry/trusted-peers"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/answer"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/audio-profiles"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/call/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/contacts"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts/check/{identity_hash}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/contacts/export"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/contacts/import"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/contacts/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/telephone/contacts/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/hangup"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/history"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/history"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/mute-receive"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/mute-transmit"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/recordings"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/recordings/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/recordings/{id}/audio/{side}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones/status"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/ringtones/upload"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/ringtones/{id}"
+ },
+ {
+ "method": "PATCH",
+ "path": "/api/v1/telephone/ringtones/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/ringtones/{id}/audio"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/send-to-voicemail"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/switch-audio-profile/{profile_id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/unmute-receive"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/unmute-transmit"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/generate-greeting"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/voicemail/greeting"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemail/greeting/audio"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/record/start"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/record/stop"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemail/greeting/upload"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemail/status"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemails"
+ },
+ {
+ "method": "DELETE",
+ "path": "/api/v1/telephone/voicemails/{id}"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/telephone/voicemails/{id}/audio"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/telephone/voicemails/{id}/read"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/tools/rnode/download_firmware"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/translator/install-languages"
+ },
+ {
+ "method": "GET",
+ "path": "/api/v1/translator/languages"
+ },
+ {
+ "method": "POST",
+ "path": "/api/v1/translator/translate"
+ },
+ {
+ "method": "GET",
+ "path": "/call.html"
+ },
+ {
+ "method": "GET",
+ "path": "/manifest.json"
+ },
+ {
+ "method": "GET",
+ "path": "/service-worker.js"
+ },
+ {
+ "method": "GET",
+ "path": "/ws"
+ },
+ {
+ "method": "GET",
+ "path": "/ws/telephone/audio"
+ }
+ ]
}

diff --git a/tests/backend/test_access_attempts_dao.py b/tests/backend/test_access_attempts_dao.py
index d3202007..00078323 100644
--- a/tests/backend/test_access_attempts_dao.py
+++ b/tests/backend/test_access_attempts_dao.py
@@ -10,9 +10,9 @@ from hypothesis import strategies as st
from meshchatx.src.backend.database.access_attempts import (
LOGIN_PATH,
+ MAX_TRUSTED_FINGERPRINTS_PER_IDENTITY,
SETUP_PATH,
AccessAttemptsDAO,
- MAX_TRUSTED_FINGERPRINTS_PER_IDENTITY,
user_agent_hash,
)
@@ -65,7 +65,7 @@ def test_count_matches_list_for_no_filters(dao):
def test_search_filters(dao):
ih = _id_hash()
dao.insert(
- ih, "192.168.99.1", "UniqueSearchUA", LOGIN_PATH, "POST", "success", "detail-x"
+ ih, "192.168.99.1", "UniqueSearchUA", LOGIN_PATH, "POST", "success", "detail-x",
)
found = dao.list_attempts(search="UniqueSearchUA", limit=50)
assert any(r["user_agent"] == "UniqueSearchUA" for r in found)

diff --git a/tests/backend/test_access_attempts_enforcement.py b/tests/backend/test_access_attempts_enforcement.py
index f8fed41c..8d9612a1 100644
--- a/tests/backend/test_access_attempts_enforcement.py
+++ b/tests/backend/test_access_attempts_enforcement.py
@@ -30,7 +30,7 @@ from meshchatx.src.backend.database.access_attempts import (
def _make_req(
- ip: str, ua: str, method: str = "POST", xff: str | None = None
+ ip: str, ua: str, method: str = "POST", xff: str | None = None,
) -> SimpleNamespace:
h = {"User-Agent": ua}
if xff is not None:

diff --git a/tests/backend/test_announce_fuzzing.py b/tests/backend/test_announce_fuzzing.py
index e772efa3..45b505d4 100644
--- a/tests/backend/test_announce_fuzzing.py
+++ b/tests/backend/test_announce_fuzzing.py
@@ -1,6 +1,7 @@
-import pytest
from unittest.mock import MagicMock
+import pytest
+
from meshchatx.src.backend.announce_manager import AnnounceManager
@@ -53,7 +54,7 @@ def test_announce_max_stored_config_fuzz(max_stored):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"new_dest", "lxmf.delivery", b"app_data", b"packet"
+ reticulum, identity, b"new_dest", "lxmf.delivery", b"app_data", b"packet",
)
mock_db.announces.upsert_announce.assert_called_once()

diff --git a/tests/backend/test_announce_limits.py b/tests/backend/test_announce_limits.py
index baa386ca..ca3f9b73 100644
--- a/tests/backend/test_announce_limits.py
+++ b/tests/backend/test_announce_limits.py
@@ -39,7 +39,7 @@ def test_trim_called_when_over_max_stored(mock_db, mock_config):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"new_dest", "lxmf.delivery", b"app_data", b"packet_hash"
+ reticulum, identity, b"new_dest", "lxmf.delivery", b"app_data", b"packet_hash",
)
mock_db.announces.upsert_announce.assert_called_once()
@@ -57,7 +57,7 @@ def test_no_trim_without_config(mock_db):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"dest", "lxmf.delivery", b"app_data", b"packet"
+ reticulum, identity, b"dest", "lxmf.delivery", b"app_data", b"packet",
)
mock_db.announces.upsert_announce.assert_called_once()
@@ -74,7 +74,7 @@ def test_max_stored_none_skips_trim(mock_db, mock_config):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"dest", "lxmf.delivery", b"app_data", b"packet"
+ reticulum, identity, b"dest", "lxmf.delivery", b"app_data", b"packet",
)
mock_db.announces.trim_announces_for_aspect.assert_not_called()
@@ -147,7 +147,7 @@ def test_announce_handles_none_packet_hash(mock_db):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"dest", "lxmf.delivery", b"app_data", None
+ reticulum, identity, b"dest", "lxmf.delivery", b"app_data", None,
)
mock_db.announces.upsert_announce.assert_called_once()

diff --git a/tests/backend/test_announce_manager_extended.py b/tests/backend/test_announce_manager_extended.py
index bb7e7dc7..ffab9fc6 100644
--- a/tests/backend/test_announce_manager_extended.py
+++ b/tests/backend/test_announce_manager_extended.py
@@ -1,6 +1,8 @@
-import pytest
import base64
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.announce_manager import AnnounceManager
@@ -24,7 +26,7 @@ def test_upsert_announce(mock_db):
identity.get_public_key.return_value = b"pub_key"
manager.upsert_announce(
- reticulum, identity, b"dest_hash", "aspect", b"app_data", b"packet_hash"
+ reticulum, identity, b"dest_hash", "aspect", b"app_data", b"packet_hash",
)
mock_db.announces.upsert_announce.assert_called_once()
@@ -52,7 +54,7 @@ def test_get_filtered_announces_count(mock_db):
manager = AnnounceManager(mock_db)
mock_db.provider.fetchone.return_value = {"count": 5}
count = manager.get_filtered_announces_count(
- aspect="test", query="q", blocked_identity_hashes=["b1"]
+ aspect="test", query="q", blocked_identity_hashes=["b1"],
)
assert count == 5

diff --git a/tests/backend/test_api_json_contracts.py b/tests/backend/test_api_json_contracts.py
new file mode 100644
index 00000000..842a7df5
--- /dev/null
+++ b/tests/backend/test_api_json_contracts.py
@@ -0,0 +1,146 @@
+"""JSON Schema contract tests for core HTTP API responses."""
+
+from __future__ import annotations
+
+import json
+import shutil
+import tempfile
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+import RNS
+
+from meshchatx.meshchat import ReticulumMeshChat
+from tests.backend.api_json_contract_schemas import (
+ API_V1_APP_INFO_ENVELOPE_SCHEMA,
+ API_V1_STATUS_SCHEMA,
+ AUTH_STATUS_SCHEMA,
+ assert_matches_schema,
+)
+
+
+@pytest.fixture(autouse=True)
+def _stub_threads_for_http_contract_tests():
+ with patch("threading.Thread"):
+ yield
+
+
+@pytest.fixture
+def temp_dir():
+ dir_path = tempfile.mkdtemp()
+ yield dir_path
+ shutil.rmtree(dir_path)
+
+
+@pytest.fixture
+def mock_rns_minimal():
+ with (
+ patch("RNS.Reticulum") as mock_rns,
+ patch("RNS.Transport"),
+ patch("LXMF.LXMRouter"),
+ patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
+ ):
+ mock_rns_instance = mock_rns.return_value
+ mock_rns_instance.configpath = "/tmp/mock_config"
+ mock_rns_instance.is_connected_to_shared_instance = False
+ mock_rns_instance.transport_enabled.return_value = True
+
+ mock_id = MagicMock(spec=RNS.Identity)
+ mock_id.hash = b"test_hash_32_bytes_long_01234567"
+ mock_id.hexhash = mock_id.hash.hex()
+ mock_id.get_private_key.return_value = b"test_private_key"
+ yield mock_id
+
+
+def _route_handler(app: ReticulumMeshChat, path: str, method: str):
+ for route in app.get_routes():
+ if route.path == path and route.method == method:
+ return route.handler
+ return None
+
+
+@pytest.mark.asyncio
+async def test_api_v1_status_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _route_handler(app_instance, "/api/v1/status", "GET")
+ assert handler is not None
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, API_V1_STATUS_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_app_info_json_contract(mock_rns_minimal, temp_dir):
+ with (
+ patch("meshchatx.meshchat.generate_ssl_certificate"),
+ patch("psutil.Process") as mock_process,
+ patch("psutil.net_io_counters") as mock_net_io,
+ patch("importlib.metadata.version", return_value="1.2.3"),
+ patch("meshchatx.meshchat.LXST") as mock_lxst,
+ ):
+ mock_lxst.__version__ = "1.2.3"
+
+ mock_proc_instance = mock_process.return_value
+ mock_proc_instance.memory_info.return_value.rss = 1024
+ mock_proc_instance.memory_info.return_value.vms = 2048
+ mock_proc_instance.net_connections.return_value = []
+
+ mock_net_instance = mock_net_io.return_value
+ mock_net_instance.bytes_sent = 0
+ mock_net_instance.bytes_recv = 0
+ mock_net_instance.packets_sent = 0
+ mock_net_instance.packets_recv = 0
+
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+
+ handler = _route_handler(app_instance, "/api/v1/app/info", "GET")
+ assert handler is not None
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, API_V1_APP_INFO_ENVELOPE_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_auth_status_json_contract(mock_rns_minimal, temp_dir):
+ with (
+ patch("meshchatx.meshchat.generate_ssl_certificate"),
+ patch("meshchatx.meshchat.get_session", new_callable=AsyncMock) as mock_session,
+ ):
+ mock_session.return_value = {
+ "authenticated": False,
+ "identity_hash": None,
+ }
+
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+
+ handler = _route_handler(app_instance, "/api/v1/auth/status", "GET")
+ assert handler is not None
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, AUTH_STATUS_SCHEMA)
+
+
+def test_auth_status_schema_accepts_error_envelope():
+ sample = {
+ "auth_enabled": True,
+ "password_set": True,
+ "authenticated": False,
+ "error": "decryption failed",
+ }
+ assert_matches_schema(sample, AUTH_STATUS_SCHEMA)

diff --git a/tests/backend/test_archiver_manager_extended.py b/tests/backend/test_archiver_manager_extended.py
index 4d387007..a460ba27 100644
--- a/tests/backend/test_archiver_manager_extended.py
+++ b/tests/backend/test_archiver_manager_extended.py
@@ -1,5 +1,7 @@
-import pytest
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.archiver_manager import ArchiverManager
@@ -50,7 +52,7 @@ def test_archive_page_enforce_max_versions(mock_db):
# Should delete the 6th version (index 5)
mock_db.provider.execute.assert_any_call(
- "DELETE FROM archived_pages WHERE id = ?", (6,)
+ "DELETE FROM archived_pages WHERE id = ?", (6,),
)
@@ -67,5 +69,5 @@ def test_archive_page_enforce_storage_limit(mock_db):
manager.archive_page("dest", "/path", "content", max_storage_gb=1)
mock_db.provider.execute.assert_any_call(
- "DELETE FROM archived_pages WHERE id = ?", (10,)
+ "DELETE FROM archived_pages WHERE id = ?", (10,),
)

diff --git a/tests/backend/test_bot_handler_extended.py b/tests/backend/test_bot_handler_extended.py
index 4043f291..99f73cce 100644
--- a/tests/backend/test_bot_handler_extended.py
+++ b/tests/backend/test_bot_handler_extended.py
@@ -1,6 +1,8 @@
import os
-import pytest
from unittest.mock import MagicMock, patch
+
+import pytest
+
from meshchatx.src.backend.bot_handler import BotHandler
@@ -102,7 +104,7 @@ def test_restore_enabled_bots(temp_identity_dir):
"name": "N",
"enabled": True,
"storage_dir": "/tmp/b1",
- }
+ },
]
with patch.object(handler, "start_bot") as mock_start:
handler.restore_enabled_bots()

diff --git a/tests/backend/test_community_interfaces_directory.py b/tests/backend/test_community_interfaces_directory.py
index f920ac4d..4fd96735 100644
--- a/tests/backend/test_community_interfaces_directory.py
+++ b/tests/backend/test_community_interfaces_directory.py
@@ -171,7 +171,7 @@ def test_transform_tcp_row_with_numeric_id():
"typeName": "TCPClientInterface",
"host": "10.0.0.1",
"port": 4242,
- }
+ },
]
out = transform_directory_rows(rows)
assert len(out) == 1
@@ -202,7 +202,7 @@ def test_transform_tcp_with_backbone_in_config_and_identity():
st.fixed_dictionaries(
{
"id": st.one_of(
- st.none(), st.integers(min_value=-1000, max_value=10000)
+ st.none(), st.integers(min_value=-1000, max_value=10000),
),
"name": st.text(max_size=40),
"type": st.sampled_from(["backbone", "tcp", "i2p", "rnode", ""]),

diff --git a/tests/backend/test_concurrency_stress.py b/tests/backend/test_concurrency_stress.py
index 6d430aed..cd11ba87 100644
--- a/tests/backend/test_concurrency_stress.py
+++ b/tests/backend/test_concurrency_stress.py
@@ -3,8 +3,9 @@ import secrets
import shutil
import tempfile
import threading
-import unittest
import time
+import unittest
+
from meshchatx.src.backend.database import Database
from meshchatx.src.backend.database.provider import DatabaseProvider
from meshchatx.src.backend.identity_manager import IdentityManager
@@ -69,8 +70,8 @@ class TestConcurrencyStress(unittest.TestCase):
def db_reader_worker(self, worker_id):
"""Spams the message table with reads and searches."""
try:
- from meshchatx.src.backend.database.messages import MessageDAO
from meshchatx.src.backend.database.announces import AnnounceDAO
+ from meshchatx.src.backend.database.messages import MessageDAO
provider = DatabaseProvider.get_instance(self.db_path)
msg_dao = MessageDAO(provider)
@@ -107,10 +108,10 @@ class TestConcurrencyStress(unittest.TestCase):
# Check if we ended up with the expected number of messages
total = self.db.provider.fetchone(
- "SELECT COUNT(*) as count FROM lxmf_messages"
+ "SELECT COUNT(*) as count FROM lxmf_messages",
)["count"]
self.assertEqual(
- total, 5 * 50, "Total messages inserted doesn't match expected count"
+ total, 5 * 50, "Total messages inserted doesn't match expected count",
)
print(f"Stress test completed. Total messages inserted: {total}")
@@ -131,7 +132,7 @@ class TestConcurrencyStress(unittest.TestCase):
id_thread = threading.Thread(target=identity_worker)
db_thread = threading.Thread(
- target=self.db_writer_worker, args=("id_collision",)
+ target=self.db_writer_worker, args=("id_collision",),
)
id_thread.start()
@@ -148,10 +149,10 @@ class TestConcurrencyStress(unittest.TestCase):
self.assertEqual(len(identities), 20, "Should have created 20 identities")
total_messages = self.db.provider.fetchone(
- "SELECT COUNT(*) as count FROM lxmf_messages"
+ "SELECT COUNT(*) as count FROM lxmf_messages",
)["count"]
self.assertEqual(
- total_messages, 50, "Should have inserted 50 messages during collision test"
+ total_messages, 50, "Should have inserted 50 messages during collision test",
)

diff --git a/tests/backend/test_contacts_dao_boost.py b/tests/backend/test_contacts_dao_boost.py
index b922accc..0b62c46b 100644
--- a/tests/backend/test_contacts_dao_boost.py
+++ b/tests/backend/test_contacts_dao_boost.py
@@ -1,5 +1,7 @@
-import pytest
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.database.contacts import ContactsDAO

diff --git a/tests/backend/test_name_overwrite_fixes.py b/tests/backend/test_contacts_display_name_semantics.py
similarity index 95%
rename from tests/backend/test_name_overwrite_fixes.py
rename to tests/backend/test_contacts_display_name_semantics.py
index a2fb0d06..0489466f 100644
--- a/tests/backend/test_name_overwrite_fixes.py
+++ b/tests/backend/test_contacts_display_name_semantics.py
@@ -1,11 +1,7 @@
-"""Tests for the contact/conversation name overwrite bug fixes.
-
-Covers:
-- Announce upsert preserving app_data via COALESCE when new value is NULL
-- Contacts DAO upsert conflict behaviour
-- Custom display name lifecycle (set, get, delete, re-set)
-- Contact name update propagation
-- Edge cases: empty strings, unicode, very long names, concurrent upserts
+"""Contacts and announces: display names, custom labels, and announce app_data upserts.
+
+Covers announce upsert semantics when app_data is NULL (COALESCE), contacts DAO
+conflict behaviour, custom display name lifecycle, and related edge cases.
"""
import base64
@@ -75,13 +71,12 @@ def _base_announce(dest="d" * 32, app_data="some_data"):
# ---------------------------------------------------------------------------
-# Announce upsert COALESCE tests
+# Announce upsert when app_data is NULL (COALESCE)
# ---------------------------------------------------------------------------
-class TestAnnounceUpsertPreservesAppData:
- """The core bug: upserting an announce with app_data=None must NOT wipe
- the previously stored app_data."""
+class TestAnnounceUpsertNullAppDataCoalesce:
+ """Upsert with app_data=None must not clear an existing app_data value."""
def test_null_app_data_preserves_existing(self, announce_dao):
announce_dao.upsert_announce(_base_announce(app_data="original_name"))
@@ -128,10 +123,10 @@ class TestAnnounceUpsertPreservesAppData:
def test_other_fields_still_update_on_null_app_data(self, announce_dao):
dest = "c" * 32
announce_dao.upsert_announce(
- {**_base_announce(dest=dest, app_data="keep_me"), "rssi": -50}
+ {**_base_announce(dest=dest, app_data="keep_me"), "rssi": -50},
)
announce_dao.upsert_announce(
- {**_base_announce(dest=dest, app_data=None), "rssi": -90}
+ {**_base_announce(dest=dest, app_data=None), "rssi": -90},
)
row = announce_dao.get_announce_by_hash(dest)
assert row["app_data"] == "keep_me"
@@ -208,33 +203,33 @@ class TestCustomDisplayNameLifecycle:
class TestContactsEdgeCases:
def test_add_contact_upsert_preserves_addresses(self, contacts_dao, provider):
contacts_dao.add_contact(
- "Alice", "ih1", lxmf_address="lxmf1", lxst_address="lxst1"
+ "Alice", "ih1", lxmf_address="lxmf1", lxst_address="lxst1",
)
contacts_dao.add_contact(
- "Alice Updated", "ih1", lxmf_address=None, lxst_address=None
+ "Alice Updated", "ih1", lxmf_address=None, lxst_address=None,
)
row = provider.fetchone(
- "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih1",)
+ "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih1",),
)
assert row["name"] == "Alice Updated"
assert row["lxmf_address"] == "lxmf1"
assert row["lxst_address"] == "lxst1"
def test_add_contact_upsert_replaces_name_unconditionally(
- self, contacts_dao, provider
+ self, contacts_dao, provider,
):
"""Verifies add_contact always overwrites name on conflict."""
contacts_dao.add_contact("Real Name", "ih2")
contacts_dao.add_contact("Overwritten", "ih2")
row = provider.fetchone(
- "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih2",)
+ "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih2",),
)
assert row["name"] == "Overwritten"
def test_update_contact_partial(self, contacts_dao, provider):
contacts_dao.add_contact("Alice", "ih3", lxmf_address="lx3")
row = provider.fetchone(
- "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih3",)
+ "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih3",),
)
cid = row["id"]
contacts_dao.update_contact(cid, name="Alice Renamed")
@@ -245,7 +240,7 @@ class TestContactsEdgeCases:
def test_update_contact_no_fields_is_noop(self, contacts_dao, provider):
contacts_dao.add_contact("NoOp", "ih4")
row = provider.fetchone(
- "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih4",)
+ "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih4",),
)
contacts_dao.update_contact(row["id"])
updated = provider.fetchone("SELECT * FROM contacts WHERE id = ?", (row["id"],))
@@ -253,10 +248,10 @@ class TestContactsEdgeCases:
def test_update_contact_clear_image(self, contacts_dao, provider):
contacts_dao.add_contact(
- "WithImage", "ih5", custom_image="data:image/png;base64,abc"
+ "WithImage", "ih5", custom_image="data:image/png;base64,abc",
)
row = provider.fetchone(
- "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih5",)
+ "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih5",),
)
assert row is not None
cid = row["id"]
@@ -282,7 +277,7 @@ class TestContactsEdgeCases:
def test_unicode_contact_name(self, contacts_dao, provider):
contacts_dao.add_contact("\u00c9milie \u00d6sterreich", "ih8")
row = provider.fetchone(
- "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih8",)
+ "SELECT * FROM contacts WHERE remote_identity_hash = ?", ("ih8",),
)
assert row["name"] == "\u00c9milie \u00d6sterreich"
@@ -347,13 +342,13 @@ class TestParseLxmfDisplayNameFallback:
class TestNameResolutionPriority:
"""Simulates the priority chain:
- custom_display_name > announce app_data > contact name > 'Anonymous Peer'
+ custom_display_name > announce app_data > contact name > 'Anonymous Peer'.
"""
def test_custom_name_wins_over_announce(self, announce_dao):
dest = "f" * 32
announce_dao.upsert_announce(
- _base_announce(dest=dest, app_data="AnnounceAlice")
+ _base_announce(dest=dest, app_data="AnnounceAlice"),
)
announce_dao.upsert_custom_display_name(dest, "CustomAlice")
@@ -381,7 +376,7 @@ class TestNameResolutionPriority:
assert display == "Anonymous Peer"
def test_contact_name_used_when_no_announce_no_custom(
- self, announce_dao, contacts_dao, provider
+ self, announce_dao, contacts_dao, provider,
):
dest = "f" * 32
announce_dao.upsert_announce(_base_announce(dest=dest, app_data=None))
@@ -406,10 +401,11 @@ class TestNameResolutionPriority:
assert display == "AnnounceName"
def test_wiping_announce_with_contact_still_resolves(
- self, announce_dao, contacts_dao
+ self, announce_dao, contacts_dao,
):
"""After the COALESCE fix, this should not happen, but if app_data
- was already NULL, the contact name should still be available."""
+ was already NULL, the contact name should still be available.
+ """
dest = "f" * 32
contacts_dao.add_contact("ContactFallback", dest, lxmf_address=dest)
announce_dao.upsert_announce(_base_announce(dest=dest, app_data=None))
@@ -431,7 +427,7 @@ class TestAnnounceTrimSafety:
def test_trim_does_not_remove_active_announce(self, announce_dao):
for i in range(5):
announce_dao.upsert_announce(
- _base_announce(dest=f"{i:032x}", app_data=f"name_{i}")
+ _base_announce(dest=f"{i:032x}", app_data=f"name_{i}"),
)
announce_dao.trim_announces_for_aspect("lxmf.delivery", max_rows=3)
remaining = announce_dao.get_announces(aspect="lxmf.delivery")
@@ -448,11 +444,11 @@ class TestAnnounceTrimSafety:
class TestContactCustomNameSync:
def test_renaming_contact_and_custom_name_independently(
- self, contacts_dao, announce_dao, provider
+ self, contacts_dao, announce_dao, provider,
):
contacts_dao.add_contact("Alice", "ih20", lxmf_address="lx20")
row = provider.fetchone(
- "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih20",)
+ "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih20",),
)
cid = row["id"]
@@ -467,7 +463,7 @@ class TestContactCustomNameSync:
"""Simulates what the UI should do: update both contact and custom name."""
contacts_dao.add_contact("Bob", "ih21", lxmf_address="lx21")
row = provider.fetchone(
- "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih21",)
+ "SELECT id FROM contacts WHERE remote_identity_hash = ?", ("ih21",),
)
cid = row["id"]

diff --git a/tests/backend/test_contacts_export_import.py b/tests/backend/test_contacts_export_import.py
index db2e347d..fc1c049b 100644
--- a/tests/backend/test_contacts_export_import.py
+++ b/tests/backend/test_contacts_export_import.py
@@ -123,8 +123,8 @@ async def test_contacts_import_valid(mock_rns_minimal, temp_dir):
"remote_identity_hash": "e" * 32,
"lxmf_address": "f" * 32,
},
- ]
- }
+ ],
+ },
)
response = await handler(request)
data = json.loads(response.body)
@@ -160,8 +160,8 @@ async def test_contacts_import_skips_invalid(mock_rns_minimal, temp_dir):
{"name": "Valid", "remote_identity_hash": "a" * 32},
{"name": ""},
{"remote_identity_hash": "b" * 32},
- ]
- }
+ ],
+ },
)
response = await handler(request)
data = json.loads(response.body)

diff --git a/tests/backend/test_crash_history.py b/tests/backend/test_crash_history.py
index db59ff7a..12fd00a6 100644
--- a/tests/backend/test_crash_history.py
+++ b/tests/backend/test_crash_history.py
@@ -6,7 +6,7 @@ import time
import unittest
from meshchatx.src.backend.database import Database
-from meshchatx.src.backend.recovery.crash_recovery import CrashRecovery, _DEFAULT_PRIORS
+from meshchatx.src.backend.recovery.crash_recovery import _DEFAULT_PRIORS, CrashRecovery
class TestCrashHistoryDAO(unittest.TestCase):

diff --git a/tests/backend/test_crash_recovery.py b/tests/backend/test_crash_recovery.py
index 03109f4c..fa1e4bcf 100644
--- a/tests/backend/test_crash_recovery.py
+++ b/tests/backend/test_crash_recovery.py
@@ -442,7 +442,7 @@ class TestCrashRecovery(unittest.TestCase):
def test_diagnosis_empty_db_file(self):
"""0-byte database file should trigger a warning."""
- open(self.db_path, "w").close() # noqa: SIM115
+ open(self.db_path, "w").close()
output = io.StringIO()
self.recovery.run_diagnosis(file=output)

diff --git a/tests/backend/test_dao_fuzzing.py b/tests/backend/test_dao_fuzzing.py
index 356fab88..c3fc2701 100644
--- a/tests/backend/test_dao_fuzzing.py
+++ b/tests/backend/test_dao_fuzzing.py
@@ -30,7 +30,6 @@ from meshchatx.src.backend.meshchat_utils import (
)
from meshchatx.src.backend.message_handler import MessageHandler
-
# ---------------------------------------------------------------------------
# Strategies
# ---------------------------------------------------------------------------
@@ -60,7 +59,7 @@ st_sql_payloads = st.sampled_from(
"1=1",
"admin'--",
"' AND 1=CONVERT(int,(SELECT TOP 1 table_name FROM information_schema.tables))--",
- ]
+ ],
)
st_search_term = st.one_of(st_nasty_text, st_sql_payloads)
@@ -182,7 +181,7 @@ class TestConfigDAOFuzzing:
db.config.set(key, "injected?")
assert db.config.get(key) == "injected?"
tables = db.provider.fetchall(
- "SELECT name FROM sqlite_master WHERE type='table'"
+ "SELECT name FROM sqlite_master WHERE type='table'",
)
table_names = {r["name"] for r in tables}
assert "config" in table_names
@@ -227,7 +226,7 @@ class TestMiscDAOFuzzing:
max_examples=40,
)
def test_add_notification_never_crashes(
- self, db, ntype, remote_hash, title, content
+ self, db, ntype, remote_hash, title, content,
):
db.misc.add_notification(ntype, remote_hash, title, content)
notifications = db.misc.get_notifications()
@@ -260,7 +259,7 @@ class TestMiscDAOFuzzing:
def test_archived_pages_search_never_crashes(self, db, query, dest):
results = db.misc.get_archived_pages_paginated(
destination_hash=dest,
- query=query if query else None,
+ query=query or None,
)
assert isinstance(results, list)
@@ -464,7 +463,7 @@ class TestMapDrawingsDAOFuzzing:
st.builds(
json.dumps,
st.dictionaries(
- st.text(max_size=10), st.text(max_size=50), max_size=10
+ st.text(max_size=10), st.text(max_size=50), max_size=10,
),
),
),
@@ -573,7 +572,7 @@ class TestMessageHandlerFuzzing:
max_examples=40,
)
def test_get_conversation_messages_never_crashes(
- self, handler, dest, after_id, before_id
+ self, handler, dest, after_id, before_id,
):
results = handler.get_conversation_messages(
"local_hash",
@@ -633,7 +632,7 @@ class TestSafeHrefFuzzing:
url=st.from_regex(
r"(javascript|data|vbscript|file):[A-Za-z0-9()=;,/+]+",
fullmatch=True,
- )
+ ),
)
@settings(deadline=None, max_examples=100)
def test_safe_href_blocks_all_generated_dangerous_urls(self, url):
@@ -641,8 +640,8 @@ class TestSafeHrefFuzzing:
@given(
scheme=st.text(min_size=1, max_size=20).filter(
- lambda s: ":" not in s and "/" not in s
- )
+ lambda s: ":" not in s and "/" not in s,
+ ),
)
@settings(deadline=None, max_examples=60)
def test_safe_href_blocks_unknown_schemes(self, scheme):
@@ -711,7 +710,7 @@ class TestMessageFieldsHaveAttachments:
st.text(max_size=20),
st.one_of(st.text(max_size=50), st.integers(), st.booleans(), st.none()),
max_size=10,
- )
+ ),
)
@settings(deadline=None, max_examples=80)
def test_never_crashes_on_arbitrary_json_objects(self, obj):
@@ -743,7 +742,7 @@ class TestMessageFieldsHaveAttachments:
| st.dictionaries(st.text(max_size=10), children, max_size=5)
),
max_leaves=30,
- )
+ ),
)
@settings(deadline=None, max_examples=60)
def test_deeply_nested_json_never_crashes(self, data):

diff --git a/tests/backend/test_database_evolution.py b/tests/backend/test_database_evolution.py
index 4e72ad1c..21f6faaf 100644
--- a/tests/backend/test_database_evolution.py
+++ b/tests/backend/test_database_evolution.py
@@ -3,9 +3,10 @@ import shutil
import sqlite3
import tempfile
import unittest
+
from meshchatx.src.backend.database import Database
-from meshchatx.src.backend.database.provider import DatabaseProvider
from meshchatx.src.backend.database.legacy_migrator import LegacyMigrator
+from meshchatx.src.backend.database.provider import DatabaseProvider
class TestDatabaseMigration(unittest.TestCase):
@@ -16,7 +17,7 @@ class TestDatabaseMigration(unittest.TestCase):
self.identity_hash = "deadbeef"
self.legacy_config_dir = os.path.join(self.test_dir, "legacy_config")
self.legacy_db_subdir = os.path.join(
- self.legacy_config_dir, "identities", self.identity_hash
+ self.legacy_config_dir, "identities", self.identity_hash,
)
os.makedirs(self.legacy_db_subdir, exist_ok=True)
self.legacy_db_path = os.path.join(self.legacy_db_subdir, "database.db")
@@ -118,12 +119,12 @@ class TestDatabaseMigration(unittest.TestCase):
def test_migration_evolution(self):
migrator = LegacyMigrator(
- self.db.provider, self.legacy_config_dir, self.identity_hash
+ self.db.provider, self.legacy_config_dir, self.identity_hash,
)
# Check if should migrate
self.assertTrue(
- migrator.should_migrate(), "Should detect legacy database for migration"
+ migrator.should_migrate(), "Should detect legacy database for migration",
)
# Perform migration
@@ -135,18 +136,18 @@ class TestDatabaseMigration(unittest.TestCase):
print(f"Config rows: {config_rows}")
config_val = self.db.provider.fetchone(
- "SELECT value FROM config WHERE key = ?", ("legacy_key",)
+ "SELECT value FROM config WHERE key = ?", ("legacy_key",),
)
self.assertIsNotNone(config_val, "legacy_key should have been migrated")
self.assertEqual(config_val["value"], "legacy_value")
ann_count = self.db.provider.fetchone(
- "SELECT COUNT(*) as count FROM announces"
+ "SELECT COUNT(*) as count FROM announces",
)["count"]
self.assertEqual(ann_count, 1)
msg = self.db.provider.fetchone(
- "SELECT * FROM lxmf_messages WHERE hash = ?", ("msg1",)
+ "SELECT * FROM lxmf_messages WHERE hash = ?", ("msg1",),
)
self.assertIsNotNone(msg)
self.assertEqual(msg["title"], "Old Title")

diff --git a/tests/backend/test_database_provider_boost.py b/tests/backend/test_database_provider_boost.py
index 2ea494f3..d254da37 100644
--- a/tests/backend/test_database_provider_boost.py
+++ b/tests/backend/test_database_provider_boost.py
@@ -1,6 +1,8 @@
-import pytest
import sqlite3
import threading
+
+import pytest
+
from meshchatx.src.backend.database.provider import DatabaseProvider

diff --git a/tests/backend/test_database_snapshots.py b/tests/backend/test_database_snapshots.py
index 62b149f5..1b87097a 100644
--- a/tests/backend/test_database_snapshots.py
+++ b/tests/backend/test_database_snapshots.py
@@ -119,7 +119,7 @@ def test_backup_suspicious_when_messages_gone_skips_cleanup_and_baseline(temp_di
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
- }
+ },
)
result1 = db.backup_database(temp_dir, max_count=3)
assert result1.get("suspicious") is not True
@@ -173,7 +173,7 @@ def test_backup_suspicious_when_size_collapsed_skips_cleanup(temp_dir):
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
- }
+ },
)
db.backup_database(temp_dir, max_count=3)
baseline_path = os.path.join(temp_dir, "database-backups", "backup-baseline.json")
@@ -256,7 +256,7 @@ def test_check_db_health_at_open_baseline_suspicious_content(temp_dir):
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
- }
+ },
)
db.backup_database(temp_dir)
db.messages.delete_all_lxmf_messages()

diff --git a/tests/backend/test_display_name_fuzzing.py b/tests/backend/test_display_name_fuzzing.py
index e69cd845..08f1a8b6 100644
--- a/tests/backend/test_display_name_fuzzing.py
+++ b/tests/backend/test_display_name_fuzzing.py
@@ -1,11 +1,14 @@
import base64
-from hypothesis import given, strategies as st, settings, HealthCheck
+
import RNS.vendor.umsgpack as msgpack
+from hypothesis import HealthCheck, given, settings
+from hypothesis import strategies as st
+
from meshchatx.src.backend.meshchat_utils import (
parse_lxmf_display_name,
- parse_nomadnetwork_node_display_name,
parse_lxmf_propagation_node_app_data,
parse_lxmf_stamp_cost,
+ parse_nomadnetwork_node_display_name,
)
# Strategies for generating diverse display names
@@ -22,7 +25,7 @@ st_display_name = st.one_of(
@st.composite
def st_lxmf_announce_app_data(draw):
- """Generates valid LXMF announce app_data (msgpack list [name, ...])"""
+ """Generates valid LXMF announce app_data (msgpack list [name, ...])."""
name = draw(st_display_name)
# LXMF announces are usually [display_name, stamp_cost, propagation_node_data, ...]
# We'll generate lists of various lengths
@@ -72,7 +75,7 @@ def test_parse_lxmf_display_name_invalid_base64(data):
@given(app_data=st_lxmf_announce_app_data())
def test_parse_lxmf_display_name_logic_check(app_data):
- """Verify that if we can manually unpack it, parse_lxmf_display_name matches our expectation"""
+ """Verify that if we can manually unpack it, parse_lxmf_display_name matches our expectation."""
try:
unpacked = msgpack.unpackb(app_data)
if isinstance(unpacked, list) and len(unpacked) >= 1:

diff --git a/tests/backend/test_docs_manager.py b/tests/backend/test_docs_manager.py
index b0c29c62..4090e11c 100644
--- a/tests/backend/test_docs_manager.py
+++ b/tests/backend/test_docs_manager.py
@@ -105,7 +105,7 @@ def test_download_task_success(mock_session_cls, docs_manager, temp_dirs):
yield b"data" * 25
mock_response.content.iter_chunked = MagicMock(
- side_effect=lambda n: iter_chunked(n)
+ side_effect=lambda n: iter_chunked(n),
)
mock_get = MagicMock()

diff --git a/tests/backend/test_fuzzing_extended.py b/tests/backend/test_fuzzing_extended.py
index 27c38b77..8f413e2d 100644
--- a/tests/backend/test_fuzzing_extended.py
+++ b/tests/backend/test_fuzzing_extended.py
@@ -364,7 +364,7 @@ def test_markdown_renderer_fuzzing(text):
"[](" * 500 + ")" * 500,
"\x00\x01\x02\n\t",
"\ufffd" * 100,
- ]
+ ],
),
),
)

diff --git a/tests/backend/test_hex_identifier_utils.py b/tests/backend/test_hex_identifier_utils.py
index c9dce42a..274e4a2b 100644
--- a/tests/backend/test_hex_identifier_utils.py
+++ b/tests/backend/test_hex_identifier_utils.py
@@ -1,3 +1,6 @@
+from hypothesis import given
+from hypothesis import strategies as st
+
from meshchatx.src.backend.meshchat_utils import (
hex_identifier_to_bytes,
normalize_hex_identifier,
@@ -32,3 +35,34 @@ def test_hex_identifier_to_bytes_invalid_returns_none():
assert hex_identifier_to_bytes("") is None
assert hex_identifier_to_bytes(None) is None
assert hex_identifier_to_bytes("abc") is None
+
+
+@given(s=st.text())
+def test_normalize_hex_identifier_never_raises(s):
+ normalize_hex_identifier(s)
+
+
+@given(s=st.text())
+def test_hex_identifier_to_bytes_never_raises(s):
+ hex_identifier_to_bytes(s)
+
+
+@given(h=st.from_regex(r"[0-9a-fA-F]{0,200}"))
+def test_hex_identifier_length_invariant(h):
+ n = normalize_hex_identifier(h)
+ b = hex_identifier_to_bytes(h)
+ if not n or len(n) % 2:
+ assert b is None
+ else:
+ assert b is not None
+ assert len(b) == len(n) // 2
+
+
+@given(
+ a=st.from_regex(r"[0-9a-fA-F]{2,64}"),
+ b=st.from_regex(r"[0-9a-fA-F]{2,64}"),
+)
+def test_normalize_concat_equals_normalize_join(a, b):
+ assert normalize_hex_identifier(a + b) == normalize_hex_identifier(
+ normalize_hex_identifier(a) + normalize_hex_identifier(b),
+ )

diff --git a/tests/backend/test_http_api_contract.py b/tests/backend/test_http_api_contract.py
index ad9f93fa..584f2890 100644
--- a/tests/backend/test_http_api_contract.py
+++ b/tests/backend/test_http_api_contract.py
@@ -24,7 +24,7 @@ def test_meshchat_http_routes_match_fixture():
if os.environ.get("UPDATE_HTTP_API_ROUTES") == "1":
write_route_fixture(_FIXTURE, live)
pytest.skip(
- "UPDATE_HTTP_API_ROUTES=1: fixture updated; re-run without the env var"
+ "UPDATE_HTTP_API_ROUTES=1: fixture updated; re-run without the env var",
)
expected = load_route_fixture(_FIXTURE)
assert live == expected, (

diff --git a/tests/backend/test_http_auth_security.py b/tests/backend/test_http_auth_security.py
index 0ba9c08e..b796498d 100644
--- a/tests/backend/test_http_auth_security.py
+++ b/tests/backend/test_http_auth_security.py
@@ -1,5 +1,6 @@
import asyncio
import secrets
+
import bcrypt
import pytest
from aiohttp import web

diff --git a/tests/backend/test_https_wss_side_sniffing.py b/tests/backend/test_https_wss_side_sniffing.py
index 39dd3a09..e0eab066 100644
--- a/tests/backend/test_https_wss_side_sniffing.py
+++ b/tests/backend/test_https_wss_side_sniffing.py
@@ -1,5 +1,4 @@
-"""
-Tests that HTTPS/WSS is used so local traffic cannot be sniffed by other apps.
+"""Tests that HTTPS/WSS is used so local traffic cannot be sniffed by other apps.
Server must speak TLS only on the API/WS port; plain HTTP must not be accepted.
"""
@@ -75,8 +74,7 @@ def ssl_context_and_cert(temp_storage):
async def test_https_serves_over_tls_only_plain_http_gets_no_http_response(
ssl_context_and_cert,
):
- """
- Server started with ssl_context must not respond to plain HTTP.
+ """Server started with ssl_context must not respond to plain HTTP.
A local sniffer connecting with raw TCP and sending HTTP would get
TLS handshake bytes or connection closure, not plaintext HTTP response.
"""
@@ -112,7 +110,7 @@ async def test_https_serves_over_tls_only_plain_http_gets_no_http_response(
sock.sendall(b"GET / HTTP/1.0\r\n\r\n")
try:
raw = sock.recv(1024)
- except socket.timeout:
+ except TimeoutError:
raw = b""
finally:
sock.close()
@@ -126,8 +124,7 @@ async def test_https_serves_over_tls_only_plain_http_gets_no_http_response(
@pytest.mark.asyncio
async def test_wss_over_same_tls_port(ssl_context_and_cert):
- """
- WebSocket upgrade over the same TLS port uses WSS (encrypted).
+ """WebSocket upgrade over the same TLS port uses WSS (encrypted).
Verifies that a WS endpoint is reachable only via TLS.
"""
ssl_context, _, _ = ssl_context_and_cert
@@ -150,12 +147,11 @@ async def test_wss_over_same_tls_port(ssl_context_and_cert):
client_ctx.check_hostname = False
client_ctx.verify_mode = ssl.CERT_NONE
- async with aiohttp.ClientSession() as session:
- async with session.ws_connect(
- f"wss://127.0.0.1:{port}/ws",
- ssl=client_ctx,
- ) as ws:
- msg = await ws.receive()
+ async with aiohttp.ClientSession() as session, session.ws_connect(
+ f"wss://127.0.0.1:{port}/ws",
+ ssl=client_ctx,
+ ) as ws:
+ msg = await ws.receive()
assert msg.type in (
aiohttp.WSMsgType.CLOSE,
aiohttp.WSMsgType.CLOSING,

diff --git a/tests/backend/test_incoming_call_policy.py b/tests/backend/test_incoming_call_policy.py
index c4c76c2f..eb765a5f 100644
--- a/tests/backend/test_incoming_call_policy.py
+++ b/tests/backend/test_incoming_call_policy.py
@@ -1,6 +1,4 @@
-"""
-Incoming-call policy: blocked list, DND, contacts-only / block-strangers, voicemail + ring.
-"""
+"""Incoming-call policy: blocked list, DND, contacts-only / block-strangers, voicemail + ring."""
from unittest.mock import MagicMock, patch

diff --git a/tests/backend/test_integrity.py b/tests/backend/test_integrity.py
index b29b6ea9..e753bb87 100644
--- a/tests/backend/test_integrity.py
+++ b/tests/backend/test_integrity.py
@@ -1,7 +1,7 @@
import shutil
+import sqlite3
import tempfile
import unittest
-import sqlite3
from pathlib import Path
from meshchatx.src.backend.integrity_manager import IntegrityManager
@@ -87,7 +87,7 @@ class TestIntegrityManager(unittest.TestCase):
any(
"Critical security component" in i or "File signature mismatch" in i
for i in issues
- )
+ ),
)
def test_new_identity_detected(self):

diff --git a/tests/backend/test_integrity_extensive.py b/tests/backend/test_integrity_extensive.py
index 3038bb1c..3f9c26f6 100644
--- a/tests/backend/test_integrity_extensive.py
+++ b/tests/backend/test_integrity_extensive.py
@@ -1,11 +1,13 @@
+import json
+import os
import shutil
+import sqlite3
import tempfile
import unittest
-import os
-import sqlite3
-import json
from pathlib import Path
-from hypothesis import given, strategies as st, settings, HealthCheck
+
+from hypothesis import HealthCheck, given, settings
+from hypothesis import strategies as st
from meshchatx.src.backend.integrity_manager import IntegrityManager
@@ -48,7 +50,7 @@ class TestIntegrityManagerExtensive(unittest.TestCase):
f.write(bytes(range(256)))
# log2(256) = 8
self.assertAlmostEqual(
- self.manager._calculate_entropy(max_entropy_file), 8.0, places=5
+ self.manager._calculate_entropy(max_entropy_file), 8.0, places=5,
)
@settings(suppress_health_check=[HealthCheck.too_slow], deadline=None)
@@ -123,7 +125,7 @@ class TestIntegrityManagerExtensive(unittest.TestCase):
full_path.parent.mkdir(parents=True, exist_ok=True)
full_path.touch()
self.assertTrue(
- self.manager._should_ignore(str(rel_path)), f"Failed to ignore {v}"
+ self.manager._should_ignore(str(rel_path)), f"Failed to ignore {v}",
)
def test_critical_file_protection(self):

diff --git a/tests/backend/test_interface_discovery.py b/tests/backend/test_interface_discovery.py
index d4376167..d88d0a6a 100644
--- a/tests/backend/test_interface_discovery.py
+++ b/tests/backend/test_interface_discovery.py
@@ -448,7 +448,7 @@ async def test_interface_add_discovery_payload_fuzz_tcp_client(temp_dir):
config.write_called = False
name = f"fuzz-{i}-" + "".join(
- random.choices(string.ascii_letters + string.digits, k=8)
+ random.choices(string.ascii_letters + string.digits, k=8),
)
announce = random.randint(5, 1440)
lat = random.uniform(-90, 90)

diff --git a/tests/backend/test_sql_injection_fixes.py b/tests/backend/test_legacy_migration_sql_safety.py
similarity index 96%
rename from tests/backend/test_sql_injection_fixes.py
rename to tests/backend/test_legacy_migration_sql_safety.py
index b1b94d49..94662829 100644
--- a/tests/backend/test_sql_injection_fixes.py
+++ b/tests/backend/test_legacy_migration_sql_safety.py
@@ -1,9 +1,7 @@
-"""Tests confirming SQL-injection fixes in the raw-SQL database layer.
+"""Legacy migration and schema: safe ATTACH paths, identifiers, and raw SQL helpers.
-Covers:
- - ATTACH DATABASE path escaping (single-quote doubling) in LegacyMigrator
- - Column-name identifier filtering during legacy migration
- - _validate_identifier / _ensure_column rejection in DatabaseSchema
+Covers ATTACH DATABASE path escaping in LegacyMigrator, column identifier filtering,
+and DatabaseSchema _validate_identifier / _ensure_column behaviour.
"""
import os
@@ -18,7 +16,6 @@ from meshchatx.src.backend.database.legacy_migrator import LegacyMigrator
from meshchatx.src.backend.database.provider import DatabaseProvider
from meshchatx.src.backend.database.schema import DatabaseSchema, _validate_identifier
-
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@@ -144,7 +141,7 @@ class TestAttachDatabasePathEscaping:
migrator.migrate()
tables = provider.fetchall(
- "SELECT name FROM sqlite_master WHERE type='table' AND name='config'"
+ "SELECT name FROM sqlite_master WHERE type='table' AND name='config'",
)
assert len(tables) > 0, "config table must still exist after injection attempt"
@@ -186,7 +183,7 @@ class TestLegacyColumnFiltering:
db_path = os.path.join(identity_dir, "database.db")
conn = sqlite3.connect(db_path)
conn.execute(
- 'CREATE TABLE config (key TEXT UNIQUE, value TEXT, "key; DROP TABLE config" TEXT)'
+ 'CREATE TABLE config (key TEXT UNIQUE, value TEXT, "key; DROP TABLE config" TEXT)',
)
conn.execute("INSERT INTO config (key, value) VALUES ('safe', 'data')")
conn.commit()
@@ -335,7 +332,7 @@ def test_validate_identifier_accepts_all_valid_identifiers(name):
alphabet=st.sampled_from(list(";'\"()- \t\n\r,/*")),
min_size=1,
max_size=30,
- )
+ ),
)
@settings(deadline=None)
def test_validate_identifier_rejects_pure_metacharacter_strings(name):
@@ -356,12 +353,13 @@ def test_validate_identifier_rejects_pure_metacharacter_strings(name):
),
min_size=1,
max_size=60,
- )
+ ),
)
@settings(deadline=None, suppress_health_check=[HealthCheck.too_slow])
def test_attach_path_escaping_never_breaks_sql(path_segment):
"""The quote-doubling escaping produces a string that SQLite can parse
- without breaking out of the literal, regardless of the path content."""
+ without breaking out of the literal, regardless of the path content.
+ """
safe = path_segment.replace("'", "''")
sql = f"ATTACH DATABASE '{safe}' AS test_alias"
@@ -375,10 +373,9 @@ def test_attach_path_escaping_never_breaks_sql(path_segment):
if i + 1 < len(after_open) and after_open[i + 1] == "'":
i += 2
continue
- else:
- in_literal = False
- remainder = after_open[i + 1 :]
- break
+ in_literal = False
+ remainder = after_open[i + 1 :]
+ break
i += 1
if not in_literal:

diff --git a/tests/backend/test_licenses_api.py b/tests/backend/test_licenses_api.py
index ae8faedc..23e5f542 100644
--- a/tests/backend/test_licenses_api.py
+++ b/tests/backend/test_licenses_api.py
@@ -40,7 +40,7 @@ def mock_rns_minimal():
async def test_licenses_endpoint_returns_json(mock_rns_minimal, temp_dir):
payload = {
"backend": [
- {"name": "aiohttp", "version": "1", "author": "x", "license": "Apache-2.0"}
+ {"name": "aiohttp", "version": "1", "author": "x", "license": "Apache-2.0"},
],
"frontend": [],
"meta": {

diff --git a/tests/backend/test_lxmf_reactions.py b/tests/backend/test_lxmf_reactions.py
index 4e56efa5..00f324e5 100644
--- a/tests/backend/test_lxmf_reactions.py
+++ b/tests/backend/test_lxmf_reactions.py
@@ -39,7 +39,7 @@ def test_convert_lxmf_message_to_dict_reaction_field_16():
"reaction_to": target,
"emoji": "\U0001f44d",
"sender": "f" * 32,
- }
+ },
}
out = convert_lxmf_message_to_dict(mock_msg, include_attachments=False)
@@ -59,7 +59,7 @@ def test_convert_db_lxmf_message_to_dict_reaction():
"reaction_to": target,
"emoji": "\u2764\ufe0f",
"sender": "11" * 16,
- }
+ },
}
row = {
"id": 1,

diff --git a/tests/backend/test_lxmf_utils_boost.py b/tests/backend/test_lxmf_utils_boost.py
index ac2fd251..cf7e86cf 100644
--- a/tests/backend/test_lxmf_utils_boost.py
+++ b/tests/backend/test_lxmf_utils_boost.py
@@ -1,10 +1,12 @@
from unittest.mock import MagicMock
+
+import LXMF
+
from meshchatx.src.backend.lxmf_utils import (
- convert_lxmf_state_to_string,
- convert_lxmf_method_to_string,
convert_db_lxmf_message_to_dict,
+ convert_lxmf_method_to_string,
+ convert_lxmf_state_to_string,
)
-import LXMF
def test_convert_lxmf_state_to_string():

diff --git a/tests/backend/test_lxst_telephony_profiles_contract.py b/tests/backend/test_lxst_telephony_profiles_contract.py
index 48f3c2a5..924d3705 100644
--- a/tests/backend/test_lxst_telephony_profiles_contract.py
+++ b/tests/backend/test_lxst_telephony_profiles_contract.py
@@ -1,5 +1,4 @@
-"""
-Contract tests for LXST Telephony Profiles used by /api/v1/telephone/audio-profiles and TelephoneManager.
+"""Contract tests for LXST Telephony Profiles used by /api/v1/telephone/audio-profiles and TelephoneManager.
If lxst changes profile IDs, default profile, or display names, these tests fail so UI and config can be updated.
"""

diff --git a/tests/backend/test_markdown_renderer.py b/tests/backend/test_markdown_renderer.py
index f62987d3..9f6ed053 100644
--- a/tests/backend/test_markdown_renderer.py
+++ b/tests/backend/test_markdown_renderer.py
@@ -1,5 +1,5 @@
-import unittest
import time
+import unittest
from meshchatx.src.backend.markdown_renderer import MarkdownRenderer

diff --git a/tests/backend/test_mesh_page_file_path_security.py b/tests/backend/test_mesh_page_file_path_security.py
index e2d02bb5..d1c80fb7 100644
--- a/tests/backend/test_mesh_page_file_path_security.py
+++ b/tests/backend/test_mesh_page_file_path_security.py
@@ -1,5 +1,4 @@
-"""
-Path traversal regression tests and property-based fuzzing for mesh PageNode
+"""Path traversal regression tests and property-based fuzzing for mesh PageNode
page/file APIs and normalize_page_filename.
"""
@@ -178,7 +177,7 @@ class TestPageRespondersTraversal:
assert result == b"ok"
def test_file_responder_ignores_path_prefix_in_request_path(
- self, node_dir, mock_rns
+ self, node_dir, mock_rns,
):
node = _make_node(node_dir, mock_rns)
node.setup()
@@ -190,7 +189,7 @@ class TestPageRespondersTraversal:
def test_try_serve_local_helpers_strip_traversal():
- """Regression: meshchat local serve must not join parent dirs for file names."""
+ """Local page-node serve uses basenames only; request paths must not escape dirs."""
from meshchatx.meshchat import ReticulumMeshChat
app = MagicMock(spec=ReticulumMeshChat)

diff --git a/tests/backend/test_meshchat_coverage.py b/tests/backend/test_meshchat_coverage.py
index 6a26a42d..ecaffa83 100644
--- a/tests/backend/test_meshchat_coverage.py
+++ b/tests/backend/test_meshchat_coverage.py
@@ -1,10 +1,12 @@
-import pytest
-from unittest.mock import AsyncMock, MagicMock, patch
import asyncio
import json
import os
import subprocess
+from unittest.mock import AsyncMock, MagicMock, patch
+
import LXMF
+import pytest
+
from meshchatx.meshchat import ReticulumMeshChat
from meshchatx.src.backend.lxmf_message_fields import LxmfAudioField
@@ -215,7 +217,7 @@ async def test_update_config_nomad_renderer(mock_app):
"nomad_render_html_enabled": True,
"nomad_render_plaintext_enabled": False,
"nomad_default_page_path": "/page/index.html",
- }
+ },
)
mock_app.config.nomad_render_markdown_enabled.set.assert_called_with(False)
mock_app.config.nomad_render_html_enabled.set.assert_called_with(True)
@@ -259,16 +261,15 @@ async def test_lxm_ingest_uri_lxma_adds_contact(mock_app):
with patch(
"meshchatx.meshchat.AsyncUtils.run_async",
side_effect=lambda coro: asyncio.create_task(coro),
- ):
- with patch("meshchatx.meshchat.RNS.Identity", return_value=fake_identity):
- await mock_app.on_websocket_data_received(
- mock_client,
- {
- "type": "lxm.ingest_uri",
- "uri": f"lxma://{'aa' * 16}:{'11' * 64}",
- },
- )
- await asyncio.sleep(0)
+ ), patch("meshchatx.meshchat.RNS.Identity", return_value=fake_identity):
+ await mock_app.on_websocket_data_received(
+ mock_client,
+ {
+ "type": "lxm.ingest_uri",
+ "uri": f"lxma://{'aa' * 16}:{'11' * 64}",
+ },
+ )
+ await asyncio.sleep(0)
mock_app.database.contacts.add_contact.assert_called_once_with(
"Contact aaaaaaaa",
@@ -358,10 +359,10 @@ async def test_on_lxmf_sending_state_updated(mock_app):
with (
patch(
- "meshchatx.meshchat.convert_lxmf_message_to_dict", return_value={"h": "v"}
+ "meshchatx.meshchat.convert_lxmf_message_to_dict", return_value={"h": "v"},
),
patch(
- "meshchatx.meshchat.convert_lxmf_state_to_string", return_value="delivered"
+ "meshchatx.meshchat.convert_lxmf_state_to_string", return_value="delivered",
),
patch("meshchatx.meshchat.AsyncUtils.run_async") as mock_run_async,
):
@@ -398,13 +399,12 @@ async def test_lxmf_messages_send_route(mock_app):
"destination_hash": "dest",
"content": "hello",
"fields": {},
- }
- }
+ },
+ },
)
# Since we can't easily get the handler from mock_app without full init,
# we can skip this or try to mock the internal method if it exists.
- pass
def test_on_lxmf_sending_failed_no_propagation(mock_app):
@@ -414,7 +414,7 @@ def test_on_lxmf_sending_failed_no_propagation(mock_app):
mock_app.on_lxmf_sending_failed(mock_msg)
mock_app.on_lxmf_sending_state_updated.assert_called_once_with(
- mock_msg, context=None
+ mock_msg, context=None,
)
@@ -466,12 +466,11 @@ def test_convert_webm_opus_to_ogg_ffmpeg_fails(mock_app):
def test_convert_webm_opus_to_ogg_exception(mock_app):
webm_data = b"\x1a\x45\xdf\xa3" + b"\x00" * 100
- with patch("shutil.which", return_value="/usr/bin/ffmpeg"):
- with patch(
- "subprocess.run",
- side_effect=subprocess.TimeoutExpired(cmd="ffmpeg", timeout=30),
- ):
- result = mock_app._convert_webm_opus_to_ogg(webm_data)
+ with patch("shutil.which", return_value="/usr/bin/ffmpeg"), patch(
+ "subprocess.run",
+ side_effect=subprocess.TimeoutExpired(cmd="ffmpeg", timeout=30),
+ ):
+ result = mock_app._convert_webm_opus_to_ogg(webm_data)
assert result is webm_data
@@ -513,7 +512,7 @@ async def _run_send(app, destination_hash="aa" * 16, **kwargs):
patch("meshchatx.meshchat.AsyncUtils.run_async"),
):
await app.send_message(
- destination_hash=destination_hash, content="hi", **kwargs
+ destination_hash=destination_hash, content="hi", **kwargs,
)
return fake_lxm

diff --git a/tests/backend/test_message_dao_extended.py b/tests/backend/test_message_dao_extended.py
index 7b992671..5c6b97ff 100644
--- a/tests/backend/test_message_dao_extended.py
+++ b/tests/backend/test_message_dao_extended.py
@@ -1,6 +1,8 @@
-import pytest
import json
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.database.messages import MessageDAO
@@ -29,7 +31,7 @@ def test_upsert_lxmf_message(message_dao, mock_provider):
def test_get_lxmf_message_by_hash(message_dao, mock_provider):
message_dao.get_lxmf_message_by_hash("hash1")
mock_provider.fetchone.assert_called_with(
- "SELECT * FROM lxmf_messages WHERE hash = ?", ("hash1",)
+ "SELECT * FROM lxmf_messages WHERE hash = ?", ("hash1",),
)

diff --git a/tests/backend/test_message_handler.py b/tests/backend/test_message_handler.py
index 6a0143fc..b4d23ced 100644
--- a/tests/backend/test_message_handler.py
+++ b/tests/backend/test_message_handler.py
@@ -67,7 +67,7 @@ class TestMessageHandler(unittest.TestCase):
"folder_name": None,
"failed_count": 3,
"is_contact": 0,
- }
+ },
]
result = self.handler.get_conversations("local")
self.assertEqual(len(result), 1)
@@ -81,7 +81,7 @@ class TestMessageHandler(unittest.TestCase):
def test_search_messages(self):
self.db.provider.fetchall.return_value = [
- {"peer_hash": "peer1", "max_ts": 1234567890}
+ {"peer_hash": "peer1", "max_ts": 1234567890},
]
result = self.handler.search_messages("local", "test")
self.assertEqual(len(result), 1)

diff --git a/tests/backend/test_message_handler_extended.py b/tests/backend/test_message_handler_extended.py
index 4ddfa308..b11b1f03 100644
--- a/tests/backend/test_message_handler_extended.py
+++ b/tests/backend/test_message_handler_extended.py
@@ -1,5 +1,7 @@
-import pytest
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.message_handler import MessageHandler
@@ -70,7 +72,7 @@ def test_get_conversations_base(mock_db):
def test_get_conversations_with_filters(mock_db):
handler = MessageHandler(mock_db)
handler.get_conversations(
- "local", search="test", filter_unread=True, filter_failed=True
+ "local", search="test", filter_unread=True, filter_failed=True,
)
args, _ = mock_db.provider.fetchall.call_args

diff --git a/tests/backend/test_misc_dao_extended.py b/tests/backend/test_misc_dao_extended.py
index 5c99cedd..5f8fc303 100644
--- a/tests/backend/test_misc_dao_extended.py
+++ b/tests/backend/test_misc_dao_extended.py
@@ -1,5 +1,7 @@
-import pytest
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.database.misc import MiscDAO

diff --git a/tests/backend/test_nomadnet_downloader_boost.py b/tests/backend/test_nomadnet_downloader_boost.py
index 88103f6d..6898bbdf 100644
--- a/tests/backend/test_nomadnet_downloader_boost.py
+++ b/tests/backend/test_nomadnet_downloader_boost.py
@@ -1,16 +1,16 @@
import threading
+from unittest.mock import MagicMock, patch
import pytest
import RNS
-from unittest.mock import MagicMock, patch
from meshchatx.src.backend.nomadnet_downloader import (
NomadnetDownloader,
NomadnetFileDownloader,
NomadnetPageDownloader,
+ _nomadnet_links_lock,
get_cached_active_link,
nomadnet_cached_links,
- _nomadnet_links_lock,
)
@@ -29,7 +29,7 @@ def downloader():
on_failure = MagicMock()
on_progress = MagicMock()
return NomadnetDownloader(
- b"dest", "/path", "data", on_success, on_failure, on_progress
+ b"dest", "/path", "data", on_success, on_failure, on_progress,
)
@@ -81,7 +81,7 @@ async def test_download_no_path(downloader):
):
await downloader.download(path_lookup_timeout=0.1)
downloader._download_failure_callback.assert_called_with(
- "Could not find path to destination."
+ "Could not find path to destination.",
)
@@ -120,7 +120,7 @@ async def test_download_uses_path_wait_cache_hit(downloader):
):
with patch.object(downloader, "link_established") as mock_established:
await downloader.download(
- path_lookup_timeout=5, link_establishment_timeout=5
+ path_lookup_timeout=5, link_establishment_timeout=5,
)
mock_established.assert_called_once_with(mock_link)

diff --git a/tests/backend/test_notification_unread_semantics.py b/tests/backend/test_notification_unread_semantics.py
index 182ed711..b224b354 100644
--- a/tests/backend/test_notification_unread_semantics.py
+++ b/tests/backend/test_notification_unread_semantics.py
@@ -1,5 +1,4 @@
-"""
-Regression tests for conversation/bell unread semantics: no false positives from
+"""Regression tests for conversation/bell unread semantics: no false positives from
outgoing-latest threads or inconsistent read cursors.
"""

diff --git a/tests/backend/test_notifications.py b/tests/backend/test_notifications.py
index 97fff08d..a924c33b 100644
--- a/tests/backend/test_notifications.py
+++ b/tests/backend/test_notifications.py
@@ -214,7 +214,8 @@ def test_notification_spike_fuzzing(db, num_notifs):
class TestNotificationReliability:
"""Comprehensive tests to verify notifications are accurate, reliable,
- and never produce false positives."""
+ and never produce false positives.
+ """
def test_unread_count_matches_actual_unread(self, db):
"""Unread count must exactly match unviewed notifications."""

diff --git a/tests/backend/test_package_version_resolution.py b/tests/backend/test_package_version_resolution.py
new file mode 100644
index 00000000..0455127c
--- /dev/null
+++ b/tests/backend/test_package_version_resolution.py
@@ -0,0 +1,88 @@
+"""Tests for ReticulumMeshChat.get_package_version (About page / frozen bundles)."""
+
+from __future__ import annotations
+
+import importlib.metadata
+import sys
+from unittest.mock import patch
+
+import pytest
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+
+def test_get_package_version_uses_metadata_when_available():
+ with patch(
+ "importlib.metadata.version",
+ side_effect=lambda name: "9.8.7" if name == "websockets" else "0",
+ ):
+ assert ReticulumMeshChat.get_package_version("websockets") == "9.8.7"
+
+
+def test_get_package_version_resolves_websockets_when_metadata_missing():
+ def _missing(*_a, **_k):
+ raise importlib.metadata.PackageNotFoundError
+
+ real_import = __import__
+
+ def _import_module(name, package=None):
+ if name == "websockets":
+ return real_import("websockets")
+ return real_import(name, package=package)
+
+ with (
+ patch("importlib.metadata.version", side_effect=_missing),
+ patch("importlib.metadata.distribution", side_effect=_missing),
+ patch("importlib.metadata.packages_distributions", return_value={}),
+ patch("importlib.import_module", side_effect=_import_module),
+ ):
+ v = ReticulumMeshChat.get_package_version("websockets")
+ assert v != "unknown"
+ assert v[0].isdigit()
+
+
+def test_get_package_version_resolves_lxmfy_when_metadata_missing():
+ def _missing(*_a, **_k):
+ raise importlib.metadata.PackageNotFoundError
+
+ real_import = __import__
+
+ def _import_module(name, package=None):
+ if name in ("lxmfy", "lxmfy.__version__"):
+ return real_import(name)
+ return real_import(name, package=package)
+
+ with (
+ patch("importlib.metadata.version", side_effect=_missing),
+ patch("importlib.metadata.distribution", side_effect=_missing),
+ patch("importlib.metadata.packages_distributions", return_value={}),
+ patch("importlib.import_module", side_effect=_import_module),
+ ):
+ v = ReticulumMeshChat.get_package_version("lxmfy")
+ assert v != "unknown"
+ assert v[0].isdigit()
+
+
+@pytest.mark.parametrize(
+ "package",
+ (
+ "aiohttp",
+ "aiohttp-session",
+ "cryptography",
+ "psutil",
+ "websockets",
+ "ply",
+ "bcrypt",
+ "lxmfy",
+ ),
+)
+def test_app_info_dependency_keys_resolve_in_dev_env(package: str):
+ """Regression: About backend stack must not show vunknown for bundled deps."""
+ v = ReticulumMeshChat.get_package_version(package)
+ assert v != "unknown", f"{package} must resolve when installed"
+
+
+@pytest.mark.skipif(sys.version_info < (3, 13), reason="audioop-lts only on Python 3.13+")
+def test_audioop_lts_resolves_when_applicable():
+ v = ReticulumMeshChat.get_package_version("audioop-lts")
+ assert v != "unknown"

diff --git a/tests/backend/test_performance_bottlenecks.py b/tests/backend/test_performance_bottlenecks.py
index 909a1a87..dede1e4c 100644
--- a/tests/backend/test_performance_bottlenecks.py
+++ b/tests/backend/test_performance_bottlenecks.py
@@ -1,3 +1,10 @@
+"""Wall-clock database throughput tests (large seeds + strict ms ceilings).
+
+Excluded from default `task test:be` / CI (like test_performance_hotpaths.py and
+test_memory_profiling.py). Run locally: `task test:be:perf` or
+`pytest tests/backend/test_performance_bottlenecks.py`.
+"""
+
import os
import secrets
import shutil
@@ -10,6 +17,19 @@ from meshchatx.src.backend.announce_manager import AnnounceManager
from meshchatx.src.backend.database import Database
+def _ci_ms_ceiling(local_ms: float, factor: float = 3.0) -> float:
+ """Shared runners are noisy; relax ceilings when CI is set."""
+ if os.environ.get("CI"):
+ return local_ms * factor
+ return local_ms
+
+
+def _ci_seconds_ceiling(local_s: float, factor: float = 3.0) -> float:
+ if os.environ.get("CI"):
+ return local_s * factor
+ return local_s
+
+
class TestPerformanceBottlenecks(unittest.TestCase):
def setUp(self):
self.test_dir = tempfile.mkdtemp()
@@ -68,7 +88,11 @@ class TestPerformanceBottlenecks(unittest.TestCase):
duration = (time.time() - start) * 1000
print(f"Fetch {limit} messages at offset {offset}: {duration:.2f}ms")
self.assertEqual(len(msgs), limit)
- self.assertLess(duration, 50, f"Pagination at offset {offset} is too slow!")
+ self.assertLess(
+ duration,
+ _ci_ms_ceiling(50),
+ f"Pagination at offset {offset} is too slow!",
+ )
def test_announce_flood_bottleneck(self):
"""Simulate a flood of incoming announces and measure processing time."""
@@ -108,7 +132,11 @@ class TestPerformanceBottlenecks(unittest.TestCase):
f"Processed {num_announces} announces in {duration_total:.2f}s (Avg: {avg_duration:.2f}ms/announce)",
)
- self.assertLess(avg_duration, 20, "Announce processing is too slow!")
+ self.assertLess(
+ avg_duration,
+ _ci_ms_ceiling(20),
+ "Announce processing is too slow!",
+ )
def test_announce_pagination_performance(self):
"""Test performance of announce pagination with search and filtering."""
@@ -138,7 +166,11 @@ class TestPerformanceBottlenecks(unittest.TestCase):
duration = (time.time() - start) * 1000
print(f"Filtered announce pagination (offset 1000): {duration:.2f}ms")
self.assertEqual(len(results), 50)
- self.assertLess(duration, 50, "Announce pagination is too slow!")
+ self.assertLess(
+ duration,
+ _ci_ms_ceiling(50),
+ "Announce pagination is too slow!",
+ )
def test_concurrent_announce_handling(self):
"""Test how the database handles concurrent announce insertions from multiple threads."""
@@ -180,7 +212,11 @@ class TestPerformanceBottlenecks(unittest.TestCase):
print(
f"Concurrent insertion took {duration:.2f}s for {num_threads * announces_per_thread} announces",
)
- self.assertLess(duration, 10.0, "Concurrent announce insertion is too slow!")
+ self.assertLess(
+ duration,
+ _ci_seconds_ceiling(10.0),
+ "Concurrent announce insertion is too slow!",
+ )
if __name__ == "__main__":

diff --git a/tests/backend/test_performance_hotpaths.py b/tests/backend/test_performance_hotpaths.py
index 45e3d62d..8b63d56e 100644
--- a/tests/backend/test_performance_hotpaths.py
+++ b/tests/backend/test_performance_hotpaths.py
@@ -34,7 +34,6 @@ from meshchatx.src.backend.announce_manager import AnnounceManager
from meshchatx.src.backend.database import Database
from meshchatx.src.backend.message_handler import MessageHandler
-
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
@@ -105,7 +104,7 @@ def latency_report(name, durations_ms):
ops = 1000 / avg if avg > 0 else float("inf")
print(
f" {name}: avg={avg:.2f}ms p50={p50:.2f}ms p95={p95:.2f}ms "
- f"p99={p99:.2f}ms ops/s={ops:.0f}"
+ f"p99={p99:.2f}ms ops/s={ops:.0f}",
)
return {"avg": avg, "p50": p50, "p95": p95, "p99": p99, "ops": ops}
@@ -524,7 +523,7 @@ class TestPerformanceHotPaths(unittest.TestCase):
total_ops = num_threads * msgs_per_thread
throughput = total_ops / (wall_ms / 1000)
print(
- f" Wall time: {wall_ms:.0f}ms for {total_ops} inserts ({throughput:.0f} ops/s)"
+ f" Wall time: {wall_ms:.0f}ms for {total_ops} inserts ({throughput:.0f} ops/s)",
)
latency_report("concurrent_write", all_durations)
@@ -570,7 +569,7 @@ class TestPerformanceHotPaths(unittest.TestCase):
total_ops = num_threads * announces_per_thread
throughput = total_ops / (wall_ms / 1000)
print(
- f" Wall time: {wall_ms:.0f}ms for {total_ops} upserts ({throughput:.0f} ops/s)"
+ f" Wall time: {wall_ms:.0f}ms for {total_ops} upserts ({throughput:.0f} ops/s)",
)
latency_report("concurrent_announce_write", all_durations)
@@ -649,7 +648,8 @@ class TestPerformanceHotPaths(unittest.TestCase):
def test_like_search_scaling(self):
"""Measure how LIKE search scales across different table sizes.
- This catches missing FTS indexes or query plan regressions."""
+ This catches missing FTS indexes or query plan regressions.
+ """
print("\n[Scaling] LIKE search across data sizes:")
# Message search on the existing 10k dataset
@@ -699,13 +699,13 @@ class TestPerformanceHotPaths(unittest.TestCase):
durations = []
for _ in range(5):
_, ms = timed_call(
- self.db.messages.mark_all_notifications_as_viewed, hashes
+ self.db.messages.mark_all_notifications_as_viewed, hashes,
)
durations.append(ms)
stats = latency_report("mark_viewed_200", durations)
self.assertLess(
- stats["p95"], 50, "mark_all_notifications_as_viewed(200) p95 > 50ms"
+ stats["p95"], 50, "mark_all_notifications_as_viewed(200) p95 > 50ms",
)
def test_move_conversations_to_folder_batch(self):
@@ -719,13 +719,13 @@ class TestPerformanceHotPaths(unittest.TestCase):
durations = []
for _ in range(5):
_, ms = timed_call(
- self.db.messages.move_conversations_to_folder, hashes, folder_id
+ self.db.messages.move_conversations_to_folder, hashes, folder_id,
)
durations.append(ms)
stats = latency_report("move_folder_200", durations)
self.assertLess(
- stats["p95"], 50, "move_conversations_to_folder(200) p95 > 50ms"
+ stats["p95"], 50, "move_conversations_to_folder(200) p95 > 50ms",
)
# ===================================================================
@@ -736,7 +736,7 @@ class TestPerformanceHotPaths(unittest.TestCase):
"""Verify critical indexes exist in the schema."""
print("\n[Indexes] Checking critical indexes exist:")
rows = self.db.provider.fetchall(
- "SELECT name FROM sqlite_master WHERE type='index'"
+ "SELECT name FROM sqlite_master WHERE type='index'",
)
index_names = {r["name"] for r in rows}

diff --git a/tests/backend/test_property_based.py b/tests/backend/test_property_based.py
index 82762124..3ce9532a 100644
--- a/tests/backend/test_property_based.py
+++ b/tests/backend/test_property_based.py
@@ -199,7 +199,7 @@ def test_parse_lxmf_propagation_node_app_data_robustness(data):
),
keys=st.lists(
st.text(min_size=1).filter(
- lambda x: "=" not in x and "]" not in x and x.strip()
+ lambda x: "=" not in x and "]" not in x and x.strip(),
),
min_size=1,
max_size=5,
@@ -268,7 +268,7 @@ def test_interface_config_parser_no_crash(text):
st.text(
min_size=1,
alphabet=st.characters(
- blacklist_categories=("Cc", "Cs"), blacklist_characters="[]"
+ blacklist_categories=("Cc", "Cs"), blacklist_characters="[]",
),
).filter(lambda x: x.strip() == x and x),
min_size=1,
@@ -279,7 +279,7 @@ def test_interface_config_parser_no_crash(text):
st.text(
min_size=1,
alphabet=st.characters(
- blacklist_categories=("Cc", "Cs"), blacklist_characters="[]="
+ blacklist_categories=("Cc", "Cs"), blacklist_characters="[]=",
),
).filter(lambda x: x.strip() == x and x),
min_size=1,
@@ -288,7 +288,7 @@ def test_interface_config_parser_no_crash(text):
),
values=st.lists(
st.text(alphabet=st.characters(blacklist_categories=("Cc", "Cs"))).filter(
- lambda x: "\n" not in x
+ lambda x: "\n" not in x,
),
min_size=1,
max_size=5,
@@ -335,7 +335,7 @@ def test_interface_config_parser_structured(names, keys, values):
"discovery_hash",
"transport_id",
"network_id",
- ]
+ ],
),
values=st.one_of(st.text(), st.integers(), st.none()),
max_size=12,
@@ -733,9 +733,9 @@ def test_message_fields_have_attachments_robustness(fields_json):
lxmf_fields=st.dictionaries(
keys=st.integers(),
values=st.one_of(
- st.text(), st.binary(), st.integers(), st.booleans(), st.none()
+ st.text(), st.binary(), st.integers(), st.booleans(), st.none(),
),
- )
+ ),
)
def test_has_attachments_robustness(lxmf_fields):
# Should never crash
@@ -846,11 +846,12 @@ def test_convert_db_lxmf_message_to_dict_extended_robustness(
),
)
def test_lxmf_utils_conversions_robustness(
- state_val, method_val, title, content, timestamp, fields
+ state_val, method_val, title, content, timestamp, fields,
):
- import LXMF
from unittest.mock import MagicMock
+ import LXMF
+
# Create a mock LXMessage
msg = MagicMock(spec=LXMF.LXMessage)
msg.state = state_val
@@ -898,7 +899,7 @@ def test_identity_recall_logic_robustness(hex_str):
@given(
aspect=st.sampled_from(
- ["lxmf.delivery", "lxst.telephony", "nomadnetwork.node", "unknown"]
+ ["lxmf.delivery", "lxst.telephony", "nomadnetwork.node", "unknown"],
),
data=st.binary(),
)
@@ -979,7 +980,7 @@ class TestCrashRecoveryMathProperties:
derandomize=True,
)
def test_system_entropy_always_finite(
- self, low_memory, config_missing, config_invalid, db_type, available_mem_mb
+ self, low_memory, config_missing, config_invalid, db_type, available_mem_mb,
):
"""Entropy and divergence must always be finite floats for any diagnosis."""
import math as m
@@ -1008,7 +1009,7 @@ class TestCrashRecoveryMathProperties:
"AttributeError",
"MemoryError",
"OSError",
- ]
+ ],
),
)
@settings(
@@ -1050,7 +1051,7 @@ class TestCrashRecoveryMathProperties:
@given(
counts=st.lists(
- st.integers(min_value=0, max_value=50), min_size=1, max_size=10
+ st.integers(min_value=0, max_value=50), min_size=1, max_size=10,
),
)
@settings(derandomize=True, deadline=None, max_examples=50)

diff --git a/tests/backend/test_reply_detection.py b/tests/backend/test_reply_detection.py
index 0f70427f..0288a907 100644
--- a/tests/backend/test_reply_detection.py
+++ b/tests/backend/test_reply_detection.py
@@ -1,7 +1,10 @@
-from hypothesis import given, strategies as st
+from unittest.mock import MagicMock
+
import LXMF
+from hypothesis import given
+from hypothesis import strategies as st
+
from meshchatx.meshchat import ReticulumMeshChat
-from unittest.mock import MagicMock
def get_mock_mesh_chat():

diff --git a/tests/backend/test_reticulum_live_network.py b/tests/backend/test_reticulum_live_network.py
index 3686beca..ae2697b8 100644
--- a/tests/backend/test_reticulum_live_network.py
+++ b/tests/backend/test_reticulum_live_network.py
@@ -1,5 +1,4 @@
-"""
-Optional live Reticulum smoke test.
+"""Optional live Reticulum smoke test.
Reticulum is a process-wide singleton; this test runs a short script in a
subprocess so it does not interfere with other tests.

diff --git a/tests/backend/test_rncp_handler_extended.py b/tests/backend/test_rncp_handler_extended.py
index b6c01d6c..6f564386 100644
--- a/tests/backend/test_rncp_handler_extended.py
+++ b/tests/backend/test_rncp_handler_extended.py
@@ -1,5 +1,7 @@
-import pytest
from unittest.mock import MagicMock, patch
+
+import pytest
+
from meshchatx.src.backend.rncp_handler import RNCPHandler
@@ -30,7 +32,7 @@ def test_rncp_handler_init(rncp_handler, mock_reticulum, mock_identity):
@patch("meshchatx.src.backend.rncp_handler.RNS.Destination")
@patch("meshchatx.src.backend.rncp_handler.RNS.Reticulum")
def test_setup_receive_destination(
- mock_rns_reticulum, mock_dest, mock_identity_class, rncp_handler
+ mock_rns_reticulum, mock_dest, mock_identity_class, rncp_handler,
):
mock_rns_reticulum.identitypath = "/tmp/rns/identities"
mock_id_obj = MagicMock()

diff --git a/tests/backend/test_rnstatus_formatting.py b/tests/backend/test_rnstatus_formatting.py
index a3e3af88..4727b772 100644
--- a/tests/backend/test_rnstatus_formatting.py
+++ b/tests/backend/test_rnstatus_formatting.py
@@ -1,7 +1,7 @@
from meshchatx.src.backend.rnstatus_handler import (
fmt_packet_count,
- fmt_percentage,
fmt_per_second,
+ fmt_percentage,
)

diff --git a/tests/backend/test_schema_migration_upgrade.py b/tests/backend/test_schema_migration_upgrade.py
index 75b1eb8f..5ac5535b 100644
--- a/tests/backend/test_schema_migration_upgrade.py
+++ b/tests/backend/test_schema_migration_upgrade.py
@@ -9,7 +9,7 @@ from meshchatx.src.backend.database.schema import DatabaseSchema
def _column_names(provider, table: str) -> set[str]:
cur = provider.connection.cursor()
try:
- cur.execute(f"PRAGMA table_info({table})") # noqa: S608
+ cur.execute(f"PRAGMA table_info({table})")
return {row[1] for row in cur.fetchall()}
finally:
cur.close()

diff --git a/tests/backend/test_security_fuzzing.py b/tests/backend/test_security_fuzzing.py
index fd04494b..e8ca4cdc 100644
--- a/tests/backend/test_security_fuzzing.py
+++ b/tests/backend/test_security_fuzzing.py
@@ -1394,7 +1394,7 @@ def test_nomadnet_page_archive_load_fuzzing(mock_app, archive_id):
),
)
def test_nomadnet_page_archive_add_fuzzing(
- mock_app, destination_hash, page_path, content
+ mock_app, destination_hash, page_path, content,
):
"""Fuzz nomadnet.page.archive.add WebSocket handler and archive_page."""
import asyncio
@@ -1465,7 +1465,7 @@ def test_nomadnet_file_download_fuzzing(mock_app, destination_hash, file_path):
),
)
def test_nomadnet_page_download_fuzzing(
- mock_app, destination_hash, page_path, field_data
+ mock_app, destination_hash, page_path, field_data,
):
"""Fuzz nomadnet.page.download WebSocket handler (page_path with backtick, field_data)."""
import asyncio
@@ -1627,7 +1627,7 @@ def test_messages_delete_by_hash_fuzzing(mock_app, message_hash):
@settings(suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None)
@given(
- message_hashes=st.lists(st.text(min_size=0, max_size=100), min_size=0, max_size=50)
+ message_hashes=st.lists(st.text(min_size=0, max_size=100), min_size=0, max_size=50),
)
def test_messages_delete_by_hashes_fuzzing(mock_app, message_hashes):
"""Fuzz bulk message delete by hashes."""
@@ -2284,7 +2284,7 @@ class TestLxmfFieldHardening:
pass
@settings(
- suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None
+ suppress_health_check=[HealthCheck.function_scoped_fixture], deadline=None,
)
@given(
fields_data=st.dictionaries(
@@ -2412,7 +2412,7 @@ class TestStrangerAttachmentBlocking:
"""Text-only messages from strangers are delivered normally."""
source_hash = os.urandom(16)
mock_msg = self._make_mock_message(
- source_hash=source_hash, with_attachments=False
+ source_hash=source_hash, with_attachments=False,
)
mock_app.config.block_attachments_from_strangers.get.return_value = True

diff --git a/tests/backend/test_smoke_extended.py b/tests/backend/test_smoke_extended.py
index 587a05e4..cb0454f6 100644
--- a/tests/backend/test_smoke_extended.py
+++ b/tests/backend/test_smoke_extended.py
@@ -1,9 +1,10 @@
+import os
+import sqlite3
import subprocess
import sys
-import os
-import pytest
import tempfile
-import sqlite3
+
+import pytest
def test_cli_help():
@@ -82,8 +83,8 @@ def test_markdown_renderer_smoke():
def test_config_manager_smoke():
"""Smoke test for ConfigManager basic operations."""
- from meshchatx.src.backend.database import Database
from meshchatx.src.backend.config_manager import ConfigManager
+ from meshchatx.src.backend.database import Database
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test_config.db")
@@ -113,6 +114,7 @@ def test_config_manager_smoke():
def test_telephone_manager_smoke():
"""Smoke test for TelephoneManager initialization."""
import RNS
+
from meshchatx.src.backend.telephone_manager import TelephoneManager
# Mock identity
@@ -124,9 +126,10 @@ def test_telephone_manager_smoke():
def test_voicemail_manager_smoke():
"""Smoke test for VoicemailManager initialization."""
- from meshchatx.src.backend.voicemail_manager import VoicemailManager
from unittest.mock import MagicMock
+ from meshchatx.src.backend.voicemail_manager import VoicemailManager
+
mock_db = MagicMock()
mock_config = MagicMock()
mock_tm = MagicMock()
@@ -151,9 +154,11 @@ def test_lxst_smoke():
def test_identity_context_smoke():
"""Smoke test for IdentityContext creation."""
+ from unittest.mock import MagicMock
+
import RNS
+
from meshchatx.src.backend.identity_context import IdentityContext
- from unittest.mock import MagicMock
identity = RNS.Identity()
mock_app = MagicMock()
@@ -171,9 +176,10 @@ def test_identity_context_smoke():
def test_announce_manager_smoke():
"""Smoke test for AnnounceManager."""
- from meshchatx.src.backend.announce_manager import AnnounceManager
from unittest.mock import MagicMock
+ from meshchatx.src.backend.announce_manager import AnnounceManager
+
mock_db = MagicMock()
manager = AnnounceManager(mock_db)
assert manager.db == mock_db
@@ -181,9 +187,10 @@ def test_announce_manager_smoke():
def test_rnstatus_handler_smoke():
"""Smoke test for RNStatusHandler."""
- from meshchatx.src.backend.rnstatus_handler import RNStatusHandler
from unittest.mock import MagicMock
+ from meshchatx.src.backend.rnstatus_handler import RNStatusHandler
+
mock_rns = MagicMock()
handler = RNStatusHandler(mock_rns)
assert handler.reticulum == mock_rns
@@ -191,9 +198,11 @@ def test_rnstatus_handler_smoke():
def test_lxmf_router_creation_smoke():
"""Smoke test for create_lxmf_router utility."""
+ from unittest.mock import patch
+
import RNS
+
from meshchatx.src.backend.meshchat_utils import create_lxmf_router
- from unittest.mock import patch
identity = RNS.Identity()
with tempfile.TemporaryDirectory() as tmpdir:

diff --git a/tests/backend/test_ssl_custom_args.py b/tests/backend/test_ssl_custom_args.py
index da172d75..2357bf01 100644
--- a/tests/backend/test_ssl_custom_args.py
+++ b/tests/backend/test_ssl_custom_args.py
@@ -10,15 +10,13 @@ import meshchatx.meshchat as meshchat_module
def test_ssl_cert_without_key_exits():
argv = ["meshchat", "--ssl-cert", "/tmp/custom-cert.pem"]
- with patch.object(sys, "argv", argv):
- with pytest.raises(SystemExit) as exc_info:
- meshchat_module.main()
+ with patch.object(sys, "argv", argv), pytest.raises(SystemExit) as exc_info:
+ meshchat_module.main()
assert exc_info.value.code == 2
def test_ssl_key_without_cert_exits():
argv = ["meshchat", "--ssl-key", "/tmp/custom-key.pem"]
- with patch.object(sys, "argv", argv):
- with pytest.raises(SystemExit) as exc_info:
- meshchat_module.main()
+ with patch.object(sys, "argv", argv), pytest.raises(SystemExit) as exc_info:
+ meshchat_module.main()
assert exc_info.value.code == 2

diff --git a/tests/backend/test_startup_advanced.py b/tests/backend/test_startup_advanced.py
index d61f96ee..548d58c9 100644
--- a/tests/backend/test_startup_advanced.py
+++ b/tests/backend/test_startup_advanced.py
@@ -261,7 +261,7 @@ def test_database_health_issues_set_on_setup(mock_rns, temp_dir):
mock_int_class.return_value.check_integrity.return_value = (True, [])
mock_db_instance = mock_db_class.return_value
mock_db_instance.check_db_health_at_open.return_value = [
- "Database content anomaly: test."
+ "Database content anomaly: test.",
]
mock_config = mock_config_class.return_value
mock_config.auth_session_secret.get.return_value = base64.urlsafe_b64encode(
@@ -284,7 +284,7 @@ def test_database_health_issues_set_on_setup(mock_rns, temp_dir):
)
app.run(host="127.0.0.1", port=8000, launch_browser=False, enable_https=False)
assert getattr(app, "database_health_issues", []) == [
- "Database content anomaly: test."
+ "Database content anomaly: test.",
]
app.teardown_identity()

diff --git a/tests/backend/test_sticker_utils.py b/tests/backend/test_sticker_utils.py
index f76d9507..ae6789d1 100644
--- a/tests/backend/test_sticker_utils.py
+++ b/tests/backend/test_sticker_utils.py
@@ -100,7 +100,7 @@ def test_validate_export_document_ok():
def test_validate_export_document_wrong_format():
with pytest.raises(ValueError, match="invalid_format"):
sticker_utils.validate_export_document(
- {"format": "other", "version": 1, "stickers": []}
+ {"format": "other", "version": 1, "stickers": []},
)
@@ -135,7 +135,7 @@ def test_mime_for_image_type():
st.none(),
st.text(max_size=40),
st.sampled_from(
- ["png", "jpeg", "jpg", "webp", "gif", "bmp", "svg", "image/png", ""]
+ ["png", "jpeg", "jpg", "webp", "gif", "bmp", "svg", "image/png", ""],
),
),
)

diff --git a/tests/backend/test_telemetry_dao_extended.py b/tests/backend/test_telemetry_dao_extended.py
index 376e960e..cead0c65 100644
--- a/tests/backend/test_telemetry_dao_extended.py
+++ b/tests/backend/test_telemetry_dao_extended.py
@@ -1,6 +1,8 @@
-import pytest
import json
from unittest.mock import MagicMock
+
+import pytest
+
from meshchatx.src.backend.database.telemetry import TelemetryDAO

diff --git a/tests/backend/test_telephone_api_json_contracts.py b/tests/backend/test_telephone_api_json_contracts.py
new file mode 100644
index 00000000..9f0a5829
--- /dev/null
+++ b/tests/backend/test_telephone_api_json_contracts.py
@@ -0,0 +1,174 @@
+"""JSON Schema contract tests for telephone phonebook, ringtones, and voicemail APIs."""
+
+from __future__ import annotations
+
+import json
+import shutil
+import tempfile
+from unittest.mock import MagicMock, patch
+
+import pytest
+import RNS
+
+from meshchatx.meshchat import ReticulumMeshChat
+from tests.backend.api_json_contract_schemas import (
+ TELEPHONE_CONTACT_CHECK_SCHEMA,
+ TELEPHONE_CONTACTS_LIST_SCHEMA,
+ TELEPHONE_RINGTONE_STATUS_SCHEMA,
+ TELEPHONE_RINGTONES_LIST_SCHEMA,
+ TELEPHONE_VOICEMAIL_STATUS_SCHEMA,
+ TELEPHONE_VOICEMAILS_ENVELOPE_SCHEMA,
+ assert_matches_schema,
+)
+
+
+@pytest.fixture(autouse=True)
+def _stub_threads_for_http_contract_tests():
+ with patch("threading.Thread"):
+ yield
+
+
+@pytest.fixture
+def temp_dir():
+ dir_path = tempfile.mkdtemp()
+ yield dir_path
+ shutil.rmtree(dir_path)
+
+
+@pytest.fixture
+def mock_rns_minimal():
+ with (
+ patch("RNS.Reticulum") as mock_rns,
+ patch("RNS.Transport"),
+ patch("LXMF.LXMRouter"),
+ patch("meshchatx.meshchat.get_file_path", return_value="/tmp/mock_path"),
+ ):
+ mock_rns_instance = mock_rns.return_value
+ mock_rns_instance.configpath = "/tmp/mock_config"
+ mock_rns_instance.is_connected_to_shared_instance = False
+ mock_rns_instance.transport_enabled.return_value = True
+
+ mock_id = MagicMock(spec=RNS.Identity)
+ mock_id.hash = b"test_hash_32_bytes_long_01234567"
+ mock_id.hexhash = mock_id.hash.hex()
+ mock_id.get_private_key.return_value = b"test_private_key"
+ yield mock_id
+
+
+def _find_handler(app: ReticulumMeshChat, path: str, method: str):
+ for route in app.get_routes():
+ if route.path == path and route.method == method:
+ return route.handler
+ return None
+
+
+class _Query:
+ def __init__(self, data: dict | None = None):
+ self._data = data or {}
+
+ def get(self, key, default=None):
+ return self._data.get(key, default)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_voicemail_status_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(app_instance, "/api/v1/telephone/voicemail/status", "GET")
+ assert handler is not None
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_VOICEMAIL_STATUS_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_voicemails_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(app_instance, "/api/v1/telephone/voicemails", "GET")
+ assert handler is not None
+ request = MagicMock()
+ request.query = _Query({"limit": "50", "offset": "0"})
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_VOICEMAILS_ENVELOPE_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_ringtones_list_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(app_instance, "/api/v1/telephone/ringtones", "GET")
+ assert handler is not None
+ request = MagicMock()
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_RINGTONES_LIST_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_ringtones_status_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(app_instance, "/api/v1/telephone/ringtones/status", "GET")
+ assert handler is not None
+ request = MagicMock()
+ request.query = _Query({})
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_RINGTONE_STATUS_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_contacts_list_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(app_instance, "/api/v1/telephone/contacts", "GET")
+ assert handler is not None
+ request = MagicMock()
+ request.query = _Query({"limit": "100", "offset": "0"})
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_CONTACTS_LIST_SCHEMA)
+
+
+@pytest.mark.asyncio
+async def test_api_v1_telephone_contacts_check_json_contract(mock_rns_minimal, temp_dir):
+ with patch("meshchatx.meshchat.generate_ssl_certificate"):
+ app_instance = ReticulumMeshChat(
+ identity=mock_rns_minimal,
+ storage_dir=temp_dir,
+ reticulum_config_dir=temp_dir,
+ )
+ handler = _find_handler(
+ app_instance,
+ "/api/v1/telephone/contacts/check/{identity_hash}",
+ "GET",
+ )
+ assert handler is not None
+ request = MagicMock()
+ request.match_info = {"identity_hash": "a1" * 16}
+ response = await handler(request)
+ data = json.loads(response.body)
+ assert_matches_schema(data, TELEPHONE_CONTACT_CHECK_SCHEMA)

diff --git a/tests/backend/test_telephone_manager_boost.py b/tests/backend/test_telephone_manager_boost.py
index 38392c72..cfb6e54f 100644
--- a/tests/backend/test_telephone_manager_boost.py
+++ b/tests/backend/test_telephone_manager_boost.py
@@ -1,7 +1,9 @@
-import pytest
import os
from unittest.mock import MagicMock, patch
-from meshchatx.src.backend.telephone_manager import TelephoneManager, Tee
+
+import pytest
+
+from meshchatx.src.backend.telephone_manager import Tee, TelephoneManager
@pytest.fixture

diff --git a/tests/backend/test_translator_handler_extended.py b/tests/backend/test_translator_handler_extended.py
index 6f600299..8da19e6b 100644
--- a/tests/backend/test_translator_handler_extended.py
+++ b/tests/backend/test_translator_handler_extended.py
@@ -1,6 +1,7 @@
-import pytest
from unittest.mock import AsyncMock, MagicMock, patch
+import pytest
+
from meshchatx.src.backend.translator_handler import TranslatorHandler
@@ -73,7 +74,7 @@ def test_translate_argos_cli(mock_run):
with patch("shutil.which", return_value="/usr/bin/argos-translate"):
result = handler.translate_text(
- "Hello", source_lang="en", target_lang="es", use_argos=True
+ "Hello", source_lang="en", target_lang="es", use_argos=True,
)
assert result["translated_text"] == "Hola"
@@ -81,7 +82,6 @@ def test_translate_argos_cli(mock_run):
def test_detect_language_simple():
TranslatorHandler(enabled=True)
# _detect_language is private
- pass
@patch("meshchatx.src.backend.translator_handler.aiohttp.ClientSession")

diff --git a/tests/backend/test_voicemail_manager_boost.py b/tests/backend/test_voicemail_manager_boost.py
index e5200879..22d8dae6 100644
--- a/tests/backend/test_voicemail_manager_boost.py
+++ b/tests/backend/test_voicemail_manager_boost.py
@@ -1,6 +1,8 @@
-import pytest
import os
from unittest.mock import MagicMock, patch
+
+import pytest
+
from meshchatx.src.backend.voicemail_manager import VoicemailManager
@@ -27,7 +29,7 @@ def test_find_bundled_binary_not_frozen(voicemail_manager):
def test_find_espeak_shutil(voicemail_manager):
with patch(
- "shutil.which", side_effect=lambda x: f"/usr/bin/{x}" if "espeak" in x else None
+ "shutil.which", side_effect=lambda x: f"/usr/bin/{x}" if "espeak" in x else None,
):
path = voicemail_manager._find_espeak()
assert "espeak" in path

diff --git a/tests/backend/test_websocket_scale.py b/tests/backend/test_websocket_scale.py
new file mode 100644
index 00000000..6b9840c7
--- /dev/null
+++ b/tests/backend/test_websocket_scale.py
@@ -0,0 +1,134 @@
+"""Scale and concurrency tests for websocket broadcast fan-out (core architecture)."""
+
+from __future__ import annotations
+
+import asyncio
+from unittest.mock import AsyncMock
+
+import pytest
+from hypothesis import HealthCheck, given, settings
+from hypothesis import strategies as st
+
+from meshchatx.meshchat import ReticulumMeshChat
+
+
+def _bind_real_websocket_broadcast(app):
+ return ReticulumMeshChat.websocket_broadcast.__get__(app, ReticulumMeshChat)
+
+
+@pytest.mark.asyncio
+async def test_websocket_broadcast_fanout_many_clients(mock_app):
+ mock_app.websocket_clients.clear()
+ n = 400
+ clients = []
+ for _ in range(n):
+ c = MagicWs()
+ clients.append(c)
+ mock_app.websocket_clients.extend(clients)
+
+ real = _bind_real_websocket_broadcast(mock_app)
+ payload = '{"type":"config","config":{}}'
+ await real(payload)
+
+ for c in clients:
+ assert c.send_str.await_count == 1
+ assert c.send_str.await_args[0][0] == payload
+
+
+@pytest.mark.asyncio
+async def test_websocket_broadcast_concurrent_broadcasts(mock_app):
+ mock_app.websocket_clients.clear()
+ clients = [MagicWs() for _ in range(120)]
+ mock_app.websocket_clients.extend(clients)
+ real = _bind_real_websocket_broadcast(mock_app)
+
+ await asyncio.gather(
+ real('{"type":"a"}'),
+ real('{"type":"b"}'),
+ real('{"type":"c"}'),
+ )
+
+ for c in clients:
+ assert c.send_str.await_count == 3
+
+
+@pytest.mark.asyncio
+async def test_websocket_broadcast_soak_iterations(mock_app):
+ mock_app.websocket_clients.clear()
+ clients = [MagicWs() for _ in range(80)]
+ mock_app.websocket_clients.extend(clients)
+ real = _bind_real_websocket_broadcast(mock_app)
+
+ for i in range(60):
+ await real(f'{{"type":"tick","i":{i}}}')
+
+ for c in clients:
+ assert c.send_str.await_count == 60
+
+
+@pytest.mark.asyncio
+async def test_websocket_broadcast_iterates_snapshot_not_live_list(mock_app):
+ """Mutating ``websocket_clients`` during iteration (e.g. another handler removes
+ a connection) must not skip entries; iterate a snapshot, not the live list.
+
+ Without ``list(...)``, removing a later client while iterating can skip that client
+ entirely (classic for-loop over mutating list).
+ """
+ mock_app.websocket_clients.clear()
+ clients = [MagicWs() for _ in range(5)]
+ lst = mock_app.websocket_clients
+ lst.extend(clients)
+
+ async def remove_last_client(_data):
+ await asyncio.sleep(0)
+ if clients[4] in lst:
+ lst.remove(clients[4])
+
+ clients[1].send_str = AsyncMock(side_effect=remove_last_client)
+ real = _bind_real_websocket_broadcast(mock_app)
+ await real("x")
+ assert clients[4].send_str.await_count == 1
+ for c in clients[:4]:
+ assert c.send_str.await_count == 1
+
+
+@pytest.mark.asyncio
+async def test_websocket_broadcast_drops_dead_clients(mock_app):
+ mock_app.websocket_clients.clear()
+ bad = MagicWs()
+ bad.send_str = AsyncMock(side_effect=RuntimeError("closed"))
+ good = MagicWs()
+ mock_app.websocket_clients.extend([bad, good])
+
+ real = _bind_real_websocket_broadcast(mock_app)
+ await real('{"type":"ping"}')
+
+ assert bad not in mock_app.websocket_clients
+ assert good in mock_app.websocket_clients
+ assert good.send_str.await_count == 1
+
+
+class MagicWs:
+ def __init__(self):
+ self.send_str = AsyncMock(return_value=None)
+
+
+@settings(
+ suppress_health_check=[HealthCheck.function_scoped_fixture],
+ max_examples=20,
+ deadline=None,
+)
+@given(
+ n=st.integers(min_value=1, max_value=128),
+ payload=st.text(min_size=0, max_size=512),
+)
+@pytest.mark.asyncio
+async def test_websocket_broadcast_fanout_property(mock_app, n, payload):
+ mock_app.websocket_clients.clear()
+ clients = [MagicWs() for _ in range(n)]
+ mock_app.websocket_clients.extend(clients)
+ real = _bind_real_websocket_broadcast(mock_app)
+ await real(payload)
+ for c in clients:
+ assert c.send_str.await_count == 1
+ assert c.send_str.await_args[0][0] == payload


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────